Compare commits
351 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e6bafe719 | |||
5f0ddc2f28 | |||
ba1c1ec8c6 | |||
67cafd8cbb | |||
ca88eb1cad | |||
2bf34fd04c | |||
b5da30fd9a | |||
95fffbbf42 | |||
9a636d5b50 | |||
2c8954c2b4 | |||
985a703811 | |||
37dbe35222 | |||
f0de24d161 | |||
7d5ff55803 | |||
7fffe05751 | |||
2ac38efef6 | |||
5881e593b2 | |||
64a4499544 | |||
fca9e1c325 | |||
6a88003b09 | |||
08b358c192 | |||
62d9bcff81 | |||
08f9163605 | |||
b507624d91 | |||
d8358ec25c | |||
e673a12beb | |||
18269423a5 | |||
1d82799400 | |||
3a9fdebb4d | |||
f8ef17e3f5 | |||
8b2f37d3d5 | |||
16dfe7524f | |||
3d399635d7 | |||
8bfc201d1f | |||
bb8e131464 | |||
05bbe05979 | |||
17a2199f36 | |||
84db6503f9 | |||
5b5f356863 | |||
d07571519e | |||
6464343428 | |||
534b45d7dd | |||
3ff8ccf58d | |||
67d2a3e398 | |||
c1f8a13888 | |||
e11af82443 | |||
5b1d5502d8 | |||
f48d86fe56 | |||
877964be1f | |||
62d983ef2a | |||
8b7a4ee8c1 | |||
7ebcfa7bc3 | |||
c98f53a151 | |||
94025b52e2 | |||
213d2f3f6a | |||
438a22bd86 | |||
e6fb8437b3 | |||
8b6166f48e | |||
3f89f2c1c5 | |||
c6ebb179a1 | |||
14fa450e3e | |||
448f40a5d8 | |||
4ecdcec17c | |||
261b88ada6 | |||
5ea0a1b07d | |||
3b8d32913e | |||
3928431ac7 | |||
6ecf403a4f | |||
1f7ee20c1e | |||
19c30c8701 | |||
dabb25b3d3 | |||
78593200eb | |||
7649d30b55 | |||
6af8dd3c1f | |||
861a8afc47 | |||
f0b2470039 | |||
c0ad20d54c | |||
9aea73e134 | |||
b34d3f89f9 | |||
5600948d8a | |||
d722bcb46f | |||
081c95190f | |||
d1f4e2132f | |||
5317d96243 | |||
8bb20ffe81 | |||
03b16db10a | |||
f0d4915fba | |||
6e8da50b35 | |||
615372aad5 | |||
08d47b0f63 | |||
08b5445838 | |||
fa07d16cf4 | |||
327688d79e | |||
1662d814ec | |||
a3d39a81d0 | |||
cb31c847d5 | |||
f579370841 | |||
2a542e0026 | |||
ca51256a16 | |||
65e17da386 | |||
1cf21be185 | |||
413e176741 | |||
39b2397d5e | |||
2ed01dffbd | |||
d0ee597416 | |||
be85530000 | |||
55ff199269 | |||
02615af1fd | |||
6594ffa87e | |||
6295dc68ef | |||
8fb0d30e1d | |||
0fb0230cb1 | |||
bdb1090664 | |||
3ce7746489 | |||
7978050f8a | |||
77ee36fa72 | |||
0fc1ec3086 | |||
f60c03ac56 | |||
b570d8809d | |||
3f268ec034 | |||
ba161ef6d1 | |||
0755d4413c | |||
459cb13957 | |||
a5ad7d4d6e | |||
3d5e76f078 | |||
78c53ae429 | |||
e794b2649c | |||
50f6fe5bd8 | |||
fc3681f41f | |||
a2369b4211 | |||
bfea49ffae | |||
8c7063d340 | |||
651150760c | |||
3d203e2da9 | |||
3798f438cf | |||
4ace8317fe | |||
ab4a9bb922 | |||
a9ef07ce78 | |||
561e41c760 | |||
9896a09576 | |||
82840fa6cb | |||
2c87cb71df | |||
709f622547 | |||
b20264b382 | |||
17cce2d702 | |||
a2b2ddc9ce | |||
cb57d893e0 | |||
9e8d66cec1 | |||
39fe833198 | |||
e7688a95fd | |||
d2cc98b04d | |||
9e887580af | |||
92feac572e | |||
fd02a40783 | |||
7c787a0e0f | |||
6a7c076203 | |||
af3abbcb4d | |||
4859c37780 | |||
19e65802f6 | |||
2cf1547423 | |||
63305f91c7 | |||
e875826250 | |||
0a44c1630f | |||
5cffc03ef6 | |||
d638e633ba | |||
61e44f2aba | |||
347b1176d2 | |||
7c0ad5b601 | |||
62851bbd60 | |||
b3a6d08656 | |||
5409548348 | |||
57552c1a6a | |||
aace3bb2a3 | |||
4899003766 | |||
0c991715ab | |||
72e0caa84a | |||
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 | |||
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 |
Cargo.lockCargo.tomlREADME.md
engine
graphics
physics
session
settings
integration-testing
lib
bsp_loader
common
Cargo.toml
src
deferred_loader
Cargo.toml
src
fixed_wide
linear_ops
ratio_ops
rbx_loader
rbxassetid
roblox_emulator
2780
Cargo.lock
generated
2780
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -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"
|
||||
@ -17,3 +24,7 @@ resolver = "2"
|
||||
#lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[profile.dev]
|
||||
strip = false
|
||||
opt-level = 3
|
||||
|
@ -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 = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||
ddsfile = "0.5.1"
|
||||
glam = "0.30.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 = "25.0.0"
|
8
engine/graphics/LICENSE
Normal file
8
engine/graphics/LICENSE
Normal file
@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
@ -1,9 +1,15 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashSet,HashMap};
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_settings::settings;
|
||||
use strafesnet_session::session;
|
||||
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
|
||||
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
|
||||
use crate::model_graphics::{self,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
|
||||
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
|
||||
|
||||
pub fn required_limits()->wgpu::Limits{
|
||||
wgpu::Limits::default()
|
||||
}
|
||||
|
||||
struct Indices{
|
||||
count:u32,
|
||||
@ -97,6 +103,26 @@ impl std::default::Default for GraphicsCamera{
|
||||
}
|
||||
}
|
||||
|
||||
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
|
||||
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
|
||||
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
|
||||
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
|
||||
for mi in instances{
|
||||
//model transform
|
||||
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
|
||||
//normal transform
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
//color
|
||||
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
|
||||
}
|
||||
raw
|
||||
}
|
||||
|
||||
pub struct GraphicsState{
|
||||
pipelines:GraphicsPipelines,
|
||||
bind_groups:GraphicsBindGroups,
|
||||
@ -136,7 +162,7 @@ impl GraphicsState{
|
||||
pub fn clear(&mut self){
|
||||
self.models.clear();
|
||||
}
|
||||
pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){
|
||||
pub fn load_user_settings(&mut self,user_settings:&settings::UserSettings){
|
||||
self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
|
||||
}
|
||||
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
|
||||
@ -448,7 +474,7 @@ impl GraphicsState{
|
||||
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities
|
||||
let mut model_count=0;
|
||||
let mut instance_count=0;
|
||||
let uniform_buffer_binding_size=crate::setup::required_limits().max_uniform_buffer_binding_size as usize;
|
||||
let uniform_buffer_binding_size=required_limits().max_uniform_buffer_binding_size as usize;
|
||||
let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES;
|
||||
self.models.reserve(models.len());
|
||||
for model in models.into_iter(){
|
||||
@ -608,7 +634,7 @@ impl GraphicsState{
|
||||
// Create the render pipeline
|
||||
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
|
||||
label:None,
|
||||
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
|
||||
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../../../strafe-client/src/shader.wgsl"))),
|
||||
});
|
||||
|
||||
//load textures
|
||||
@ -636,10 +662,10 @@ impl GraphicsState{
|
||||
wgpu::TextureFormat::Astc{
|
||||
block:AstcBlock::B4x4,
|
||||
channel:AstcChannel::UnormSrgb,
|
||||
}=>&include_bytes!("../images/astc.dds")[..],
|
||||
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..],
|
||||
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..],
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..],
|
||||
}=>&include_bytes!("../../../strafe-client/images/astc.dds")[..],
|
||||
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..],
|
||||
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..],
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..],
|
||||
_=>unreachable!(),
|
||||
};
|
||||
|
||||
@ -682,7 +708,7 @@ impl GraphicsState{
|
||||
|
||||
//squid
|
||||
let squid_texture_view={
|
||||
let bytes=include_bytes!("../images/squid.dds");
|
||||
let bytes=include_bytes!("../../../strafe-client/images/squid.dds");
|
||||
|
||||
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
|
||||
|
||||
@ -864,7 +890,7 @@ impl GraphicsState{
|
||||
&mut self,
|
||||
device:&wgpu::Device,
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
user_settings:&crate::settings::UserSettings,
|
||||
user_settings:&settings::UserSettings,
|
||||
){
|
||||
self.depth_view=Self::create_depth_texture(config,device);
|
||||
self.camera.screen_size=glam::uvec2(config.width,config.height);
|
||||
@ -875,7 +901,7 @@ impl GraphicsState{
|
||||
view:&wgpu::TextureView,
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
frame_state:crate::session::FrameState,
|
||||
frame_state:session::FrameState,
|
||||
){
|
||||
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input
|
||||
|
||||
@ -961,22 +987,3 @@ impl GraphicsState{
|
||||
self.staging_belt.recall();
|
||||
}
|
||||
}
|
||||
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
|
||||
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
|
||||
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
|
||||
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
|
||||
for mi in instances{
|
||||
//model transform
|
||||
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
|
||||
//normal transform
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
//color
|
||||
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
|
||||
}
|
||||
raw
|
||||
}
|
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 = "2024"
|
||||
|
||||
[dependencies]
|
||||
arrayvec = "0.7.6"
|
||||
glam = "0.30.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
8
engine/physics/LICENSE
Normal file
8
engine/physics/LICENSE
Normal file
@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
@ -31,15 +31,23 @@ impl<T> Body<T>
|
||||
time,
|
||||
}
|
||||
}
|
||||
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
|
||||
//(p0,v0,a0,t0)
|
||||
//(p1,v1,a1,t1)
|
||||
VirtualBody{
|
||||
body0,
|
||||
body1:self,
|
||||
}
|
||||
}
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
let dt=time-self.time;
|
||||
self.position
|
||||
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
|
||||
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
|
||||
+(self.velocity*dt).map(|elem|elem.divide().clamp_1())
|
||||
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_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())
|
||||
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_1())
|
||||
}
|
||||
pub fn advance_time(&mut self,time:Time<T>){
|
||||
self.position=self.extrapolated_position(time);
|
||||
@ -63,12 +71,12 @@ impl<T> Body<T>
|
||||
D2:Copy,
|
||||
Planar64:core::ops::Mul<D2,Output=N4>,
|
||||
N4:integer::Divide<D2,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
T1:integer::Clamp<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
|
||||
.map(|elem|elem.divide().clamp())+self.position
|
||||
}
|
||||
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
@ -77,12 +85,12 @@ impl<T> Body<T>
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<Den,Output=N1>,
|
||||
N1:integer::Divide<Den,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
T1:integer::Clamp<Planar64>,
|
||||
{
|
||||
// a*dt + v
|
||||
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
|
||||
self.acceleration.map(|elem|(dt*elem).divide().clamp())+self.velocity
|
||||
}
|
||||
pub fn advance_time_ratio_dt(&mut self,dt:crate::model_physics::GigaTime){
|
||||
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
|
||||
self.position=self.extrapolated_position_ratio_dt(dt);
|
||||
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
|
||||
self.time+=dt.into();
|
||||
@ -137,14 +145,6 @@ pub struct VirtualBody<'a,T>{
|
||||
impl<T> VirtualBody<'_,T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
|
||||
//(p0,v0,a0,t0)
|
||||
//(p1,v1,a1,t1)
|
||||
VirtualBody{
|
||||
body0,
|
||||
body1,
|
||||
}
|
||||
}
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge};
|
||||
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
|
||||
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
|
||||
use crate::physics::{Time,Body};
|
||||
|
||||
@ -57,13 +57,14 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
}
|
||||
}
|
||||
//test each edge collision time, ignoring roots with zero or conflicting derivative
|
||||
for &directed_edge_id in mesh.face_edges(face_id).iter(){
|
||||
for &directed_edge_id in mesh.face_edges(face_id).as_ref(){
|
||||
let edge_n=mesh.directed_edge_n(directed_edge_id);
|
||||
let n=n.cross(edge_n);
|
||||
let verts=mesh.edge_verts(directed_edge_id.as_undirected());
|
||||
let &[v0,v1]=mesh.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
//WARNING: d is moved out of the *2 block because of adding two vertices!
|
||||
//WARNING: precision is swept under the rug!
|
||||
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()){
|
||||
//wrap for speed
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_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=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
@ -77,13 +78,15 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
//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);
|
||||
let delta_pos=body.position*2-(mesh.vert(edge_verts[0])+mesh.vert(edge_verts[1]));
|
||||
for (i,&edge_face_id) in mesh.edge_faces(edge_id).iter().enumerate(){
|
||||
let &[ev0,ev1]=edge_verts.as_ref();
|
||||
let delta_pos=body.position*2-(mesh.vert(ev0)+mesh.vert(ev1));
|
||||
for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
|
||||
let face_n=mesh.face_nd(edge_face_id).0;
|
||||
//edge_n gets parity from the order of edge_faces
|
||||
let n=face_n.cross(edge_n)*((i as i64)*2-1);
|
||||
//WARNING yada yada d *2
|
||||
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()){
|
||||
//wrap for speed
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_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=Transition::Next(FEV::Face(edge_face_id),dt);
|
||||
@ -92,12 +95,12 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
}
|
||||
}
|
||||
//test each vertex collision time, ignoring roots with zero or conflicting derivative
|
||||
for (i,&vert_id) in edge_verts.iter().enumerate(){
|
||||
for (i,&vert_id) in edge_verts.as_ref().iter().enumerate(){
|
||||
//vertex normal gets parity from vert index
|
||||
let n=edge_n*(1-2*(i as i64));
|
||||
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*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(){
|
||||
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
|
||||
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
|
||||
best_time=dt;
|
||||
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
|
||||
break;
|
||||
@ -108,12 +111,12 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
},
|
||||
&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(){
|
||||
for &directed_edge_id in mesh.vert_edges(vert_id).as_ref(){
|
||||
//edge is directed away from vertex, but we want the dot product to turn out negative
|
||||
let n=-mesh.directed_edge_n(directed_edge_id);
|
||||
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*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(){
|
||||
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
|
||||
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
|
||||
best_time=dt;
|
||||
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
break;
|
||||
@ -128,11 +131,11 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
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())
|
||||
Ratio::new(r.num.widen_4(),r.den.widen_4())
|
||||
};
|
||||
let time_limit={
|
||||
let r=(time_limit-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
Ratio::new(r.num.widen_4(),r.den.widen_4())
|
||||
};
|
||||
for _ in 0..20{
|
||||
match self.next_transition(body_time,mesh,relative_body,time_limit){
|
45
engine/physics/src/lib.rs
Normal file
45
engine/physics/src/lib.rs
Normal file
@ -0,0 +1,45 @@
|
||||
mod body;
|
||||
mod push_solve;
|
||||
mod face_crawler;
|
||||
mod model;
|
||||
|
||||
pub mod physics;
|
||||
|
||||
// Physics bug fixes can easily desync all bots.
|
||||
//
|
||||
// When replaying a bot, use the exact physics version which it was recorded with.
|
||||
//
|
||||
// When validating a new bot, ignore the version and use the latest version,
|
||||
// and overwrite the version in the file.
|
||||
//
|
||||
// Compatible physics versions should be determined
|
||||
// empirically at development time via leaderboard resimulation.
|
||||
//
|
||||
// Compatible physics versions should result in an identical leaderboard state,
|
||||
// or the only bots which fail are ones exploiting a surgically patched bug.
|
||||
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
|
||||
pub struct PhysicsVersion(u32);
|
||||
pub const VERSION:PhysicsVersion=PhysicsVersion(0);
|
||||
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
|
||||
let compat=[0];
|
||||
|
||||
let mut input_version=0;
|
||||
while input_version<compat.len(){
|
||||
// compatible version must be greater than or equal to the input version
|
||||
assert!(input_version as u32<=compat[input_version]);
|
||||
// compatible version must be a version that exists
|
||||
assert!(compat[input_version]<=VERSION.0);
|
||||
input_version+=1;
|
||||
}
|
||||
compat
|
||||
};
|
||||
pub enum PhysicsVersionError{
|
||||
UnknownPhysicsVersion,
|
||||
}
|
||||
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
|
||||
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
|
||||
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
|
||||
}else{
|
||||
Err(PhysicsVersionError::UnknownPhysicsVersion)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
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};
|
||||
@ -7,6 +7,13 @@ use strafesnet_common::physics::Time;
|
||||
|
||||
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
|
||||
|
||||
struct AsRefHelper<T>(T);
|
||||
impl<T> AsRef<T> for AsRefHelper<T>{
|
||||
fn as_ref(&self)->&T{
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UndirectedEdge{
|
||||
type DirectedEdge:Copy+DirectedEdge;
|
||||
fn as_directed(&self,parity:bool)->Self::DirectedEdge;
|
||||
@ -68,27 +75,27 @@ struct Face{
|
||||
}
|
||||
struct Vert(Planar64Vec3);
|
||||
pub trait MeshQuery{
|
||||
type Face:Clone;
|
||||
type Edge:Clone+DirectedEdge;
|
||||
type Vert:Clone;
|
||||
type Face:Copy;
|
||||
type Edge:Copy+DirectedEdge;
|
||||
type Vert:Copy;
|
||||
// Vertex must be Planar64Vec3 because it represents an actual position
|
||||
type Normal;
|
||||
type Offset;
|
||||
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())
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v1)-self.vert(v0)
|
||||
}
|
||||
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)
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
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]>;
|
||||
fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>;
|
||||
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Face;2]>;
|
||||
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Vert;2]>;
|
||||
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
|
||||
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
|
||||
}
|
||||
struct FaceRefs{
|
||||
edges:Vec<SubmeshDirectedEdgeId>,
|
||||
@ -371,8 +378,8 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
|
||||
}
|
||||
//assume face hash is stable, and there are no flush faces...
|
||||
let face=Face{
|
||||
normal:(normal/len as i64).divide().fix_1(),
|
||||
dot:(dot/(len*len) as i64).fix_1(),
|
||||
normal:(normal/len as i64).divide().narrow_1().unwrap(),
|
||||
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
|
||||
};
|
||||
let face_id=match face_id_from_face.get(&face){
|
||||
Some(&face_id)=>face_id,
|
||||
@ -434,20 +441,20 @@ impl MeshQuery for PhysicsMeshView<'_>{
|
||||
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
|
||||
self.data.verts[vert_idx].0
|
||||
}
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
Cow::Borrowed(&self.topology.face_topology[face_id.get() as usize].edges)
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.topology.face_topology[face_id.get() as usize].edges.as_slice()
|
||||
}
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
|
||||
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].faces)
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
|
||||
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].faces)
|
||||
}
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
|
||||
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].verts)
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
|
||||
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].verts)
|
||||
}
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].edges)
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.topology.vert_topology[vert_id.get() as usize].edges.as_slice()
|
||||
}
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
|
||||
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].faces)
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
|
||||
self.topology.vert_topology[vert_id.get() as usize].faces.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,12 +490,15 @@ impl TransformedMesh<'_>{
|
||||
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{
|
||||
self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
|
||||
}
|
||||
pub fn faces(&self)->impl Iterator<Item=SubmeshFaceId>{
|
||||
(0..self.view.topology.faces.len() as u32).map(SubmeshFaceId::new)
|
||||
}
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
|
||||
//this happens to be well-defined. there are no virtual virtices
|
||||
SubmeshVertId::new(
|
||||
self.view.topology.verts.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_,&vert_id)|
|
||||
.max_by_key(|&(_,&vert_id)|
|
||||
dir.dot(self.transform.vertex.transform_point3(self.view.data.verts[vert_id.get() as usize].0))
|
||||
)
|
||||
//assume there is more than zero vertices.
|
||||
@ -509,26 +519,27 @@ impl MeshQuery for TransformedMesh<'_>{
|
||||
(transformed_n,transformed_d)
|
||||
}
|
||||
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
|
||||
self.transform.vertex.transform_point3(self.view.vert(vert_id)).fix_1()
|
||||
// wrap for speed
|
||||
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
|
||||
}
|
||||
#[inline]
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.view.face_edges(face_id)
|
||||
}
|
||||
#[inline]
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
|
||||
self.view.edge_faces(edge_id)
|
||||
}
|
||||
#[inline]
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
|
||||
self.view.edge_verts(edge_id)
|
||||
}
|
||||
#[inline]
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.view.vert_edges(vert_id)
|
||||
}
|
||||
#[inline]
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
|
||||
self.view.vert_faces(vert_id)
|
||||
}
|
||||
}
|
||||
@ -617,12 +628,12 @@ impl MinkowskiMesh<'_>{
|
||||
}
|
||||
fn next_transition_vert(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->Transition{
|
||||
let mut best_transition=Transition::Done;
|
||||
for &directed_edge_id in self.vert_edges(vert_id).iter(){
|
||||
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//is boundary uncrossable by a crawl from infinity
|
||||
let edge_verts=self.edge_verts(directed_edge_id.as_undirected());
|
||||
//select opposite vertex
|
||||
let test_vert_id=edge_verts[directed_edge_id.parity() as usize];
|
||||
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
|
||||
//test if it's closer
|
||||
let diff=point-self.vert(test_vert_id);
|
||||
if edge_n.dot(infinity_dir).is_zero(){
|
||||
@ -638,7 +649,7 @@ impl MinkowskiMesh<'_>{
|
||||
fn final_ev(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
|
||||
let mut best_transition=EV::Vert(vert_id);
|
||||
let diff=point-self.vert(vert_id);
|
||||
for &directed_edge_id in self.vert_edges(vert_id).iter(){
|
||||
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//is boundary uncrossable by a crawl from infinity
|
||||
//check if time of collision is outside Time::MIN..Time::MAX
|
||||
@ -649,7 +660,8 @@ impl MinkowskiMesh<'_>{
|
||||
if !d.is_negative()&&d<=edge_nn{
|
||||
let distance_squared={
|
||||
let c=diff.cross(edge_n);
|
||||
(c.dot(c)/edge_nn).divide().fix_2()
|
||||
//wrap for speed
|
||||
(c.dot(c)/edge_nn).divide().wrap_2()
|
||||
};
|
||||
if distance_squared<=*best_distance_squared{
|
||||
best_transition=EV::Edge(directed_edge_id.as_undirected());
|
||||
@ -685,10 +697,10 @@ impl MinkowskiMesh<'_>{
|
||||
let edge_n=self.edge_n(edge_id);
|
||||
// point is multiplied by two because vert_sum sums two vertices.
|
||||
let delta_pos=point*2-{
|
||||
let &[v0,v1]=self.edge_verts(edge_id).borrow();
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v0)+self.vert(v1)
|
||||
};
|
||||
for (i,&face_id) in self.edge_faces(edge_id).iter().enumerate(){
|
||||
for (i,&face_id) in self.edge_faces(edge_id).as_ref().iter().enumerate(){
|
||||
let face_n=self.face_nd(face_id).0;
|
||||
//edge-face boundary nd, n facing out of the face towards the edge
|
||||
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
|
||||
@ -718,8 +730,8 @@ impl MinkowskiMesh<'_>{
|
||||
//
|
||||
// Most of the calculation time is just calculating the starting point
|
||||
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
|
||||
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time,)->Option<FEV<MinkowskiMesh>>{
|
||||
infinity_body.infinity_dir().map_or(None,|dir|{
|
||||
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
|
||||
infinity_body.infinity_dir().and_then(|dir|{
|
||||
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
|
||||
//a line is simpler to solve than a parabola
|
||||
infinity_body.velocity=dir;
|
||||
@ -729,23 +741,23 @@ impl MinkowskiMesh<'_>{
|
||||
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone(),start_time).map_or(None,|fev|{
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
|
||||
//continue forwards along the body parabola
|
||||
fev.crawl(self,relative_body,start_time,time_limit).hit()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
//create an extrapolated body at time_limit
|
||||
let infinity_body=-relative_body.clone();
|
||||
self.closest_fev_not_inside(infinity_body,-time_limit).map_or(None,|fev|{
|
||||
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
|
||||
//continue backwards along the body parabola
|
||||
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
|
||||
//no need to test -time<time_limit because of the first step
|
||||
.map(|(face,time)|(face,-time))
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_face_out(&self,relative_body:&Body,start_time:Time,time_limit:Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
|
||||
pub fn predict_collision_face_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
|
||||
//no algorithm needed, there is only one state and two cases (Edge,None)
|
||||
//determine when it passes an edge ("sliding off" case)
|
||||
let start_time={
|
||||
@ -754,19 +766,20 @@ impl MinkowskiMesh<'_>{
|
||||
};
|
||||
let mut best_time={
|
||||
let r=(time_limit-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
Ratio::new(r.num.widen_4(),r.den.widen_4())
|
||||
};
|
||||
let mut best_edge=None;
|
||||
let face_n=self.face_nd(contact_face_id).0;
|
||||
for &directed_edge_id in self.face_edges(contact_face_id).iter(){
|
||||
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//f x e points in
|
||||
let n=face_n.cross(edge_n);
|
||||
let verts=self.edge_verts(directed_edge_id.as_undirected());
|
||||
let d=n.dot(self.vert(verts[0])+self.vert(verts[1]));
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
let d=n.dot(self.vert(v0)+self.vert(v1));
|
||||
//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()){
|
||||
//wrap for speed
|
||||
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
|
||||
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);
|
||||
@ -807,12 +820,12 @@ impl MeshQuery for MinkowskiMesh<'_>{
|
||||
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
||||
let edge0_n=self.mesh0.edge_n(e0);
|
||||
let edge1_n=self.mesh1.edge_n(e1);
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).borrow();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).borrow();
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
||||
let n=edge0_n.cross(edge1_n);
|
||||
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
|
||||
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
|
||||
((n*(parity as i64*4-2)).fix_3(),((e0d-e1d)*(parity as i64*2-1)).fix_4())
|
||||
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
|
||||
},
|
||||
MinkowskiFace::FaceVert(f0,v1)=>{
|
||||
let (n,d)=self.mesh0.face_nd(f0);
|
||||
@ -827,44 +840,44 @@ impl MeshQuery for MinkowskiMesh<'_>{
|
||||
},
|
||||
}
|
||||
}
|
||||
fn face_edges(&self,face_id:MinkowskiFace)->Cow<[MinkowskiDirectedEdge]>{
|
||||
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
Cow::Owned(self.mesh1.face_edges(f1).iter().map(|&edge_id1|{
|
||||
self.mesh1.face_edges(f1).as_ref().iter().map(|&edge_id1|
|
||||
MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse())
|
||||
}).collect())
|
||||
).collect()
|
||||
},
|
||||
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
||||
let e0v=self.mesh0.edge_verts(e0);
|
||||
let e1v=self.mesh1.edge_verts(e1);
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
||||
//could sort this if ordered edges are needed
|
||||
//probably just need to reverse this list according to parity
|
||||
Cow::Owned(vec![
|
||||
MinkowskiDirectedEdge::VertEdge(e0v[0],e1.as_directed(parity)),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v[0]),
|
||||
MinkowskiDirectedEdge::VertEdge(e0v[1],e1.as_directed(!parity)),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v[1]),
|
||||
])
|
||||
vec![
|
||||
MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0),
|
||||
MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1),
|
||||
]
|
||||
},
|
||||
MinkowskiFace::FaceVert(f0,v1)=>{
|
||||
Cow::Owned(self.mesh0.face_edges(f0).iter().map(|&edge_id0|{
|
||||
self.mesh0.face_edges(f0).as_ref().iter().map(|&edge_id0|
|
||||
MinkowskiDirectedEdge::EdgeVert(edge_id0,v1)
|
||||
}).collect())
|
||||
).collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
fn edge_faces(&self,edge_id:MinkowskiEdge)->Cow<[MinkowskiFace;2]>{
|
||||
fn edge_faces(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiFace;2]>{
|
||||
match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>{
|
||||
//faces are listed backwards from the minkowski mesh
|
||||
let v0e=self.mesh0.vert_edges(v0);
|
||||
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).borrow();
|
||||
Cow::Owned([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
|
||||
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
|
||||
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
|
||||
let mut best_edge=None;
|
||||
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
|
||||
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
|
||||
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
|
||||
for &directed_edge_id0 in v0e.iter(){
|
||||
for &directed_edge_id0 in v0e.as_ref(){
|
||||
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
|
||||
//must be behind other face.
|
||||
let d=edge_face1_n.dot(edge0_n);
|
||||
@ -888,13 +901,13 @@ impl MeshQuery for MinkowskiMesh<'_>{
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>{
|
||||
//tracking index with an external variable because .enumerate() is not available
|
||||
let v1e=self.mesh1.vert_edges(v1);
|
||||
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).borrow();
|
||||
Cow::Owned([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
|
||||
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
|
||||
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
|
||||
let mut best_edge=None;
|
||||
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
|
||||
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
|
||||
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
|
||||
for &directed_edge_id1 in v1e.iter(){
|
||||
for &directed_edge_id1 in v1e.as_ref(){
|
||||
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
|
||||
let d=edge_face0_n.dot(edge1_n);
|
||||
if d.is_negative(){
|
||||
@ -914,31 +927,27 @@ impl MeshQuery for MinkowskiMesh<'_>{
|
||||
},
|
||||
}
|
||||
}
|
||||
fn edge_verts(&self,edge_id:MinkowskiEdge)->Cow<[MinkowskiVert;2]>{
|
||||
match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>{
|
||||
Cow::Owned(self.mesh1.edge_verts(e1).map(|vert_id1|{
|
||||
MinkowskiVert::VertVert(v0,vert_id1)
|
||||
}))
|
||||
},
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>{
|
||||
Cow::Owned(self.mesh0.edge_verts(e0).map(|vert_id0|{
|
||||
MinkowskiVert::VertVert(vert_id0,v1)
|
||||
}))
|
||||
},
|
||||
}
|
||||
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
|
||||
AsRefHelper(match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>(*self.mesh1.edge_verts(e1).as_ref()).map(|vert_id1|
|
||||
MinkowskiVert::VertVert(v0,vert_id1)
|
||||
),
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>(*self.mesh0.edge_verts(e0).as_ref()).map(|vert_id0|
|
||||
MinkowskiVert::VertVert(vert_id0,v1)
|
||||
),
|
||||
})
|
||||
}
|
||||
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<[MinkowskiDirectedEdge]>{
|
||||
fn vert_edges(&self,vert_id:MinkowskiVert)->impl AsRef<[MinkowskiDirectedEdge]>{
|
||||
match vert_id{
|
||||
MinkowskiVert::VertVert(v0,v1)=>{
|
||||
let mut edges=Vec::new();
|
||||
//detect shared volume when the other mesh is mirrored along a test edge dir
|
||||
let v0f=self.mesh0.vert_faces(v0);
|
||||
let v1f=self.mesh1.vert_faces(v1);
|
||||
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
|
||||
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
|
||||
let the_len=v0f.len()+v1f.len();
|
||||
for &directed_edge_id in self.mesh0.vert_edges(v0).iter(){
|
||||
let v0f_n:Vec<_>=v0f.as_ref().iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
|
||||
let v1f_n:Vec<_>=v1f.as_ref().iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
|
||||
let the_len=v0f.as_ref().len()+v1f.as_ref().len();
|
||||
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
|
||||
let n=self.mesh0.directed_edge_n(directed_edge_id);
|
||||
let nn=n.dot(n);
|
||||
// TODO: there's gotta be a better way to do this
|
||||
@ -948,30 +957,33 @@ impl MeshQuery for MinkowskiMesh<'_>{
|
||||
face_normals.clone_from(&v0f_n);
|
||||
for face_n in &v1f_n{
|
||||
//add reflected mesh1 faces
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().fix_3());
|
||||
//wrap for speed
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
|
||||
}
|
||||
if is_empty_volume(face_normals){
|
||||
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
|
||||
}
|
||||
}
|
||||
for &directed_edge_id in self.mesh1.vert_edges(v1).iter(){
|
||||
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
|
||||
let n=self.mesh1.directed_edge_n(directed_edge_id);
|
||||
let nn=n.dot(n);
|
||||
let mut face_normals=Vec::with_capacity(the_len);
|
||||
face_normals.clone_from(&v1f_n);
|
||||
for face_n in &v0f_n{
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().fix_3());
|
||||
//wrap for speed
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
|
||||
}
|
||||
if is_empty_volume(face_normals){
|
||||
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
|
||||
}
|
||||
}
|
||||
Cow::Owned(edges)
|
||||
edges
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<[MinkowskiFace]>{
|
||||
unimplemented!()
|
||||
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
|
||||
unimplemented!();
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1001,12 +1013,6 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
|
||||
|
||||
#[test]
|
||||
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);
|
||||
assert!(!is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()].to_vec()));
|
||||
assert!(is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()].to_vec()));
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use std::collections::{HashMap,HashSet};
|
||||
use crate::model_physics::{self,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
|
||||
use crate::model::{self as model_physics,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
|
||||
use strafesnet_common::bvh;
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_common::run;
|
||||
@ -14,45 +14,6 @@ use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Ma
|
||||
pub use strafesnet_common::physics::{Time,TimeInner};
|
||||
use gameplay::ModeState;
|
||||
|
||||
// Physics bug fixes can easily desync all bots.
|
||||
//
|
||||
// When replaying a bot, use the exact physics version which it was recorded with.
|
||||
//
|
||||
// When validating a new bot, ignore the version and use the latest version,
|
||||
// and overwrite the version in the file.
|
||||
//
|
||||
// Compatible physics versions should be determined
|
||||
// empirically at development time via leaderboard resimulation.
|
||||
//
|
||||
// Compatible physics versions should result in an identical leaderboard state,
|
||||
// or the only bots which fail are ones exploiting a surgically patched bug.
|
||||
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
|
||||
pub struct PhysicsVersion(u32);
|
||||
pub const VERSION:PhysicsVersion=PhysicsVersion(1);
|
||||
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
|
||||
let compat=[0,1];
|
||||
|
||||
let mut input_version=0;
|
||||
while input_version<compat.len(){
|
||||
// compatible version must be greater that or equal to the input version
|
||||
assert!(input_version as u32<=compat[input_version]);
|
||||
// compatible version must be a version that exists
|
||||
assert!(compat[input_version]<=VERSION.0);
|
||||
input_version+=1;
|
||||
}
|
||||
compat
|
||||
};
|
||||
pub enum PhysicsVersionError{
|
||||
UnknownPhysicsVersion,
|
||||
}
|
||||
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
|
||||
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
|
||||
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
|
||||
}else{
|
||||
Err(PhysicsVersionError::UnknownPhysicsVersion)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Body=crate::body::Body<TimeInner>;
|
||||
type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
|
||||
|
||||
@ -146,6 +107,7 @@ enum TransientAcceleration{
|
||||
#[derive(Clone,Debug)]
|
||||
struct ContactMoveState{
|
||||
jump_direction:JumpDirection,
|
||||
// TODO: this is bad data normalization, remove this
|
||||
contact:ContactCollision,
|
||||
target:TransientAcceleration,
|
||||
}
|
||||
@ -156,8 +118,8 @@ impl TransientAcceleration{
|
||||
}else{
|
||||
//normal friction acceleration is clippedAcceleration.dot(normal)*friction
|
||||
TransientAcceleration::Reachable{
|
||||
acceleration:target_diff.with_length(accel).divide().fix_1(),
|
||||
time:time+Time::from((target_diff.length()/accel).divide().fix_1())
|
||||
acceleration:target_diff.with_length(accel).divide().wrap_1(),
|
||||
time:time+Time::from((target_diff.length()/accel).divide().clamp_1())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -268,12 +230,6 @@ impl PhysicsModels{
|
||||
.map(|model|&model.transform),
|
||||
}
|
||||
}
|
||||
fn contact_model(&self,model_id:ContactModelId)->&ContactModel{
|
||||
&self.contact_models[&model_id]
|
||||
}
|
||||
fn intersect_model(&self,model_id:IntersectModelId)->&IntersectModel{
|
||||
&self.intersect_models[&model_id]
|
||||
}
|
||||
fn contact_attr(&self,model_id:ContactModelId)->&gameplay_attributes::ContactAttributes{
|
||||
&self.contact_attributes[&self.contact_models[&model_id].attr_id]
|
||||
}
|
||||
@ -308,7 +264,7 @@ impl PhysicsCamera{
|
||||
);
|
||||
self.clamped_mouse_pos=unclamped_mouse_pos;
|
||||
}
|
||||
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2 {
|
||||
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2{
|
||||
let a=-self.sensitivity.mul_int((self.clamped_mouse_pos+mouse_delta).as_i64vec2());
|
||||
let ax=Angle32::wrap_from_i64(a.x);
|
||||
let ay=Angle32::clamp_from_i64(a.y)
|
||||
@ -452,7 +408,7 @@ impl HitboxMesh{
|
||||
let transform=PhysicsMeshTransform::new(transform);
|
||||
let transformed_mesh=TransformedMesh::new(mesh.complete_mesh_view(),&transform);
|
||||
for vert in transformed_mesh.verts(){
|
||||
aabb.grow(vert.fix_1());
|
||||
aabb.grow(vert.narrow_1().unwrap());
|
||||
}
|
||||
Self{
|
||||
halfsize:aabb.size()>>1,
|
||||
@ -505,12 +461,12 @@ impl StyleHelper for StyleModifiers{
|
||||
}
|
||||
|
||||
fn get_y_control_dir(&self,camera:&PhysicsCamera,controls:Controls)->Planar64Vec3{
|
||||
(camera.rotation_y()*self.get_control_dir(controls)).fix_1()
|
||||
(camera.rotation_y()*self.get_control_dir(controls)).wrap_1()
|
||||
}
|
||||
|
||||
fn get_propulsion_control_dir(&self,camera:&PhysicsCamera,controls:Controls)->Planar64Vec3{
|
||||
//don't interpolate this! discrete mouse movement, constant acceleration
|
||||
(camera.rotation()*self.get_control_dir(controls)).fix_1()
|
||||
(camera.rotation()*self.get_control_dir(controls)).wrap_1()
|
||||
}
|
||||
fn calculate_mesh(&self)->HitboxMesh{
|
||||
let mesh=match self.hitbox.mesh{
|
||||
@ -601,7 +557,7 @@ impl MoveState{
|
||||
=>None,
|
||||
}
|
||||
}
|
||||
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
|
||||
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
|
||||
//check if you have a valid walk state and create an instruction
|
||||
match self{
|
||||
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{
|
||||
@ -646,12 +602,17 @@ impl MoveState{
|
||||
fn cull_velocity(&mut self,velocity:Planar64Vec3,body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState){
|
||||
//TODO: be more precise about contacts
|
||||
if set_velocity_cull(body,touching,models,hitbox_mesh,velocity){
|
||||
//TODO do better
|
||||
// TODO do better
|
||||
// TODO: unduplicate this code
|
||||
match self.get_walk_state(){
|
||||
//did you stop touching the thing you were walking on?
|
||||
// did you stop touching the thing you were walking on?
|
||||
Some(walk_state)=>if !touching.contacts.contains(&walk_state.contact){
|
||||
self.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
}else{
|
||||
// stopped touching something else while walking
|
||||
self.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
},
|
||||
// not walking, but stopped touching something
|
||||
None=>self.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state),
|
||||
}
|
||||
}
|
||||
@ -829,14 +790,14 @@ impl TouchingState{
|
||||
}).collect();
|
||||
crate::push_solve::push_solve(&contacts,acceleration)
|
||||
}
|
||||
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
|
||||
// let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
|
||||
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
|
||||
// let relative_body=body.relative_to(&Body::ZERO);
|
||||
let relative_body=body;
|
||||
for contact in &self.contacts{
|
||||
//detect face slide off
|
||||
let model_mesh=models.contact_mesh(contact);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time,collector.time(),contact.face_id).map(|(_face,time)|{
|
||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(_face,time)|{
|
||||
TimedInstruction{
|
||||
time:relative_body.time+time.into(),
|
||||
instruction:InternalInstruction::CollisionEnd(
|
||||
@ -850,7 +811,7 @@ impl TouchingState{
|
||||
//detect model collision in reverse
|
||||
let model_mesh=models.intersect_mesh(intersect);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_out(&relative_body,start_time,collector.time()).map(|(_face,time)|{
|
||||
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{
|
||||
TimedInstruction{
|
||||
time:relative_body.time+time.into(),
|
||||
instruction:InternalInstruction::CollisionEnd(
|
||||
@ -923,7 +884,7 @@ impl PhysicsState{
|
||||
fn reset_to_default(&mut self){
|
||||
*self=Self::default();
|
||||
}
|
||||
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
|
||||
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,Time>>{
|
||||
self.move_state.next_move_instruction(&self.style.strafe,self.time)
|
||||
}
|
||||
fn cull_velocity(&mut self,data:&PhysicsData,velocity:Planar64Vec3){
|
||||
@ -980,7 +941,7 @@ pub struct PhysicsData{
|
||||
impl Default for PhysicsData{
|
||||
fn default()->Self{
|
||||
Self{
|
||||
bvh:bvh::BvhNode::default(),
|
||||
bvh:bvh::BvhNode::empty(),
|
||||
models:Default::default(),
|
||||
modes:Default::default(),
|
||||
hitbox_mesh:StyleModifiers::default().calculate_mesh(),
|
||||
@ -995,21 +956,21 @@ pub struct PhysicsContext<'a>{
|
||||
// the physics consumes both Instruction and PhysicsInternalInstruction,
|
||||
// but can only emit PhysicsInternalInstruction
|
||||
impl InstructionConsumer<InternalInstruction> for PhysicsContext<'_>{
|
||||
type TimeInner=TimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,TimeInner>){
|
||||
type Time=Time;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,Time>){
|
||||
atomic_internal_instruction(&mut self.state,&self.data,ins)
|
||||
}
|
||||
}
|
||||
impl InstructionConsumer<Instruction> for PhysicsContext<'_>{
|
||||
type TimeInner=TimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,TimeInner>){
|
||||
type Time=Time;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Time>){
|
||||
atomic_input_instruction(&mut self.state,&self.data,ins)
|
||||
}
|
||||
}
|
||||
impl InstructionEmitter<InternalInstruction> for PhysicsContext<'_>{
|
||||
type TimeInner=TimeInner;
|
||||
type Time=Time;
|
||||
//this little next instruction function could cache its return value and invalidate the cached value by watching the State.
|
||||
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
|
||||
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
|
||||
next_instruction_internal(&self.state,&self.data,time_limit)
|
||||
}
|
||||
}
|
||||
@ -1017,7 +978,7 @@ impl PhysicsContext<'_>{
|
||||
pub fn run_input_instruction(
|
||||
state:&mut PhysicsState,
|
||||
data:&PhysicsData,
|
||||
instruction:TimedInstruction<Instruction,TimeInner>
|
||||
instruction:TimedInstruction<Instruction,Time>
|
||||
){
|
||||
let mut context=PhysicsContext{state,data};
|
||||
context.process_exhaustive(instruction.time);
|
||||
@ -1027,10 +988,7 @@ impl PhysicsContext<'_>{
|
||||
impl PhysicsData{
|
||||
/// use with caution, this is the only non-instruction way to mess with physics
|
||||
pub fn generate_models(&mut self,map:&map::CompleteMap){
|
||||
let mut modes=map.modes.clone();
|
||||
for mode in &mut modes.modes{
|
||||
mode.denormalize_data();
|
||||
}
|
||||
let modes=map.modes.clone().denormalize();
|
||||
let mut used_contact_attributes=Vec::new();
|
||||
let mut used_intersect_attributes=Vec::new();
|
||||
|
||||
@ -1048,31 +1006,30 @@ impl PhysicsData{
|
||||
let mut used_meshes=Vec::new();
|
||||
let mut physics_mesh_id_from_model_mesh_id=HashMap::<MeshId,PhysicsMeshId>::new();
|
||||
for (model_id,model) in map.models.iter().enumerate(){
|
||||
//TODO: use .entry().or_insert_with(||{
|
||||
let attr_id=if let Some(&attr_id)=physics_attr_id_from_model_attr_id.get(&model.attributes){
|
||||
attr_id
|
||||
}else{
|
||||
//check if it's real
|
||||
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
|
||||
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
|
||||
let attr_id=match p_attr{
|
||||
PhysicsCollisionAttributes::Contact(attr)=>{
|
||||
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
|
||||
used_contact_attributes.push(attr);
|
||||
PhysicsAttributesId::Contact(attr_id)
|
||||
},
|
||||
PhysicsCollisionAttributes::Intersect(attr)=>{
|
||||
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
|
||||
used_intersect_attributes.push(attr);
|
||||
PhysicsAttributesId::Intersect(attr_id)
|
||||
},
|
||||
};
|
||||
physics_attr_id_from_model_attr_id.insert(model.attributes,attr_id);
|
||||
Some(attr_id)
|
||||
})
|
||||
}){
|
||||
Some(attr_id)=>attr_id,
|
||||
None=>continue,
|
||||
let attr_id=match physics_attr_id_from_model_attr_id.entry(model.attributes){
|
||||
std::collections::hash_map::Entry::Occupied(entry)=>*entry.get(),
|
||||
std::collections::hash_map::Entry::Vacant(entry)=>{
|
||||
//check if it's real
|
||||
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
|
||||
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
|
||||
let attr_id=match p_attr{
|
||||
PhysicsCollisionAttributes::Contact(attr)=>{
|
||||
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
|
||||
used_contact_attributes.push(attr);
|
||||
PhysicsAttributesId::Contact(attr_id)
|
||||
},
|
||||
PhysicsCollisionAttributes::Intersect(attr)=>{
|
||||
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
|
||||
used_intersect_attributes.push(attr);
|
||||
PhysicsAttributesId::Intersect(attr_id)
|
||||
},
|
||||
};
|
||||
Some(*entry.insert(attr_id))
|
||||
})
|
||||
}){
|
||||
Some(attr_id)=>attr_id,
|
||||
None=>continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
let mesh_id=if let Some(&mesh_id)=physics_mesh_id_from_model_mesh_id.get(&model.mesh){
|
||||
@ -1132,7 +1089,7 @@ impl PhysicsData{
|
||||
let mut aabb=aabb::Aabb::default();
|
||||
let transformed_mesh=TransformedMesh::new(view,transform);
|
||||
for v in transformed_mesh.verts(){
|
||||
aabb.grow(v.fix_1());
|
||||
aabb.grow(v.narrow_1().unwrap());
|
||||
}
|
||||
(ConvexMeshId{
|
||||
model_id,
|
||||
@ -1166,7 +1123,7 @@ impl PhysicsData{
|
||||
}
|
||||
|
||||
//this is the one who asks
|
||||
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
|
||||
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
|
||||
//JUST POLLING!!! NO MUTATION
|
||||
let mut collector=instruction::InstructionCollector::new(time_limit);
|
||||
|
||||
@ -1179,15 +1136,15 @@ impl PhysicsData{
|
||||
state.body.grow_aabb(&mut aabb,state.time,collector.time());
|
||||
aabb.inflate(data.hitbox_mesh.halfsize);
|
||||
//relative to moving platforms
|
||||
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
|
||||
//let relative_body=state.body.relative_to(&Body::ZERO);
|
||||
let relative_body=&state.body;
|
||||
data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
|
||||
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
|
||||
//no checks are needed because of the time limits.
|
||||
let model_mesh=data.models.mesh(convex_mesh_id);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_in(relative_body,state.time,collector.time())
|
||||
collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time())
|
||||
//temp (?) code to avoid collision loops
|
||||
.map_or(None,|(face,dt)|{
|
||||
.and_then(|(face,dt)|{
|
||||
// this must be rounded to avoid the infinite loop when hitting the start zone
|
||||
let time=relative_body.time+dt.into();
|
||||
(state.time<time).then_some((time,face,dt))
|
||||
@ -1210,7 +1167,8 @@ fn contact_normal(models:&PhysicsModels,hitbox_mesh:&HitboxMesh,contact:&Contact
|
||||
let model_mesh=models.contact_mesh(contact);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
// TODO: normalize to i64::MAX>>1
|
||||
minkowski.face_nd(contact.face_id).0.fix_1()
|
||||
// wrap for speed
|
||||
minkowski.face_nd(contact.face_id).0.wrap_1()
|
||||
}
|
||||
|
||||
fn recalculate_touching(
|
||||
@ -1242,8 +1200,8 @@ fn recalculate_touching(
|
||||
aabb.grow(body.position);
|
||||
aabb.inflate(hitbox_mesh.halfsize);
|
||||
//relative to moving platforms
|
||||
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
|
||||
bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
|
||||
//let relative_body=state.body.relative_to(&Body::ZERO);
|
||||
bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
|
||||
//no checks are needed because of the time limits.
|
||||
let model_mesh=models.mesh(convex_mesh_id);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
@ -1294,7 +1252,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
|
||||
let mut culled=false;
|
||||
touching.contacts.retain(|contact|{
|
||||
let n=contact_normal(models,hitbox_mesh,contact);
|
||||
let r=(n.dot(v)>>52).is_positive();
|
||||
let r=n.dot(v).is_positive();
|
||||
if r{
|
||||
culled=true;
|
||||
}
|
||||
@ -1304,7 +1262,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
|
||||
culled
|
||||
}
|
||||
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){
|
||||
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);;
|
||||
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);
|
||||
}
|
||||
fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{
|
||||
//This is not correct but is better than what I have
|
||||
@ -1369,7 +1327,7 @@ fn teleport_to_spawn(
|
||||
const EPSILON:Planar64=Planar64::raw((1<<32)/16);
|
||||
let transform=models.get_model_transform(spawn_model_id).ok_or(TeleportToSpawnError::NoModel)?;
|
||||
//TODO: transform.vertex.matrix3.col(1)+transform.vertex.translation
|
||||
let point=transform.vertex.transform_point3(vec3::Y).fix_1()+Planar64Vec3::new([Planar64::ZERO,style.hitbox.halfsize.y+EPSILON,Planar64::ZERO]);
|
||||
let point=transform.vertex.transform_point3(vec3::Y).clamp_1()+Planar64Vec3::new([Planar64::ZERO,style.hitbox.halfsize.y+EPSILON,Planar64::ZERO]);
|
||||
teleport(point,move_state,body,touching,run,mode_state,Some(mode),models,hitbox_mesh,bvh,style,camera,input_state,time);
|
||||
Ok(())
|
||||
}
|
||||
@ -1533,7 +1491,7 @@ fn collision_start_contact(
|
||||
Some(gameplay_attributes::ContactingBehaviour::Surf)=>(),
|
||||
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
|
||||
&Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{
|
||||
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1();
|
||||
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).wrap_1();
|
||||
set_velocity(body,touching,models,hitbox_mesh,reflected_velocity);
|
||||
},
|
||||
Some(gameplay_attributes::ContactingBehaviour::Ladder(contacting_ladder))=>
|
||||
@ -1667,10 +1625,14 @@ fn collision_end_contact(
|
||||
//TODO do better
|
||||
//this is inner code from move_state.cull_velocity
|
||||
match move_state.get_walk_state(){
|
||||
//did you stop touching the thing you were walking on?
|
||||
// did you stop touching the thing you were walking on?
|
||||
Some(walk_state)=>if walk_state.contact==contact{
|
||||
move_state.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
}else{
|
||||
// stopped touching something else while walking
|
||||
move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
},
|
||||
// not walking, but stopped touching something
|
||||
None=>move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state),
|
||||
}
|
||||
}
|
||||
@ -1696,7 +1658,7 @@ fn collision_end_intersect(
|
||||
}
|
||||
}
|
||||
}
|
||||
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,TimeInner>){
|
||||
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,Time>){
|
||||
state.time=ins.time;
|
||||
let (should_advance_body,goober_time)=match ins.instruction{
|
||||
InternalInstruction::CollisionStart(_,dt)
|
||||
@ -1756,7 +1718,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
|
||||
let control_dir=state.style.get_control_dir(masked_controls);
|
||||
if control_dir!=vec3::ZERO{
|
||||
let camera_mat=state.camera.simulate_move_rotation_y(state.input_state.lerp_delta(state.time).x);
|
||||
if let Some(ticked_velocity)=strafe_settings.tick_velocity(state.body.velocity,(camera_mat*control_dir).with_length(Planar64::ONE).divide().fix_1()){
|
||||
if let Some(ticked_velocity)=strafe_settings.tick_velocity(state.body.velocity,(camera_mat*control_dir).with_length(Planar64::ONE).divide().wrap_1()){
|
||||
//this is wrong but will work ig
|
||||
//need to note which push planes activate in push solve and keep those
|
||||
state.cull_velocity(data,ticked_velocity);
|
||||
@ -1792,7 +1754,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
|
||||
}
|
||||
}
|
||||
|
||||
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,TimeInner>){
|
||||
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,Time>){
|
||||
state.time=ins.time;
|
||||
let should_advance_body=match ins.instruction{
|
||||
//the body may as well be a quantum wave function
|
||||
@ -1859,14 +1821,18 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
},
|
||||
Instruction::Mode(ModeInstruction::Restart(mode_id))=>{
|
||||
//teleport to mode start zone
|
||||
let mut spawn_point=vec3::ZERO;
|
||||
let mode=data.modes.get_mode(mode_id);
|
||||
let spawn_point=mode.and_then(|mode|
|
||||
if let Some(mode)=mode{
|
||||
// set style
|
||||
state.style=mode.get_style().clone();
|
||||
//TODO: spawn at the bottom of the start zone plus the hitbox size
|
||||
//TODO: set camera andles to face the same way as the start zone
|
||||
data.models.get_model_transform(mode.get_start().into()).map(|transform|
|
||||
transform.vertex.translation
|
||||
)
|
||||
).unwrap_or(vec3::ZERO);
|
||||
//TODO: set camera angles to face the same way as the start zone
|
||||
if let Some(transform)=data.models.get_model_transform(mode.get_start()){
|
||||
// NOTE: this value may be 0,0,0 on source maps
|
||||
spawn_point=transform.vertex.translation;
|
||||
}
|
||||
}
|
||||
set_position(spawn_point,&mut state.move_state,&mut state.body,&mut state.touching,&mut state.run,&mut state.mode_state,mode,&data.models,&data.hitbox_mesh,&data.bvh,&state.style,&state.camera,&state.input_state,state.time);
|
||||
set_velocity(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
|
||||
state.set_move_state(data,MoveState::Air);
|
||||
@ -1876,6 +1842,9 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
Instruction::Mode(ModeInstruction::Spawn(mode_id,stage_id))=>{
|
||||
//spawn at a particular stage
|
||||
if let Some(mode)=data.modes.get_mode(mode_id){
|
||||
// set style
|
||||
state.style=mode.get_style().clone();
|
||||
// teleport to stage in mode
|
||||
if let Some(stage)=mode.get_stage(stage_id){
|
||||
let _=teleport_to_spawn(
|
||||
stage.spawn(),
|
||||
@ -1912,8 +1881,6 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::file;
|
||||
use crate::body::VirtualBody;
|
||||
use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
|
||||
use super::*;
|
||||
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
@ -1922,7 +1889,7 @@ mod test{
|
||||
let hitbox_mesh=h1.transformed_mesh();
|
||||
let platform_mesh=h0.transformed_mesh();
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO,Time::from_secs(10));
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
|
||||
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
|
||||
}
|
||||
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
@ -1940,7 +1907,7 @@ mod test{
|
||||
let hitbox_mesh=h1.transformed_mesh();
|
||||
let platform_mesh=h0.transformed_mesh();
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO,Time::from_secs(10));
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
|
||||
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
|
||||
}
|
||||
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
@ -1994,111 +1961,111 @@ mod test{
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_west(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(3,3,0),
|
||||
int3(100,-1,0),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_north(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,3,3),
|
||||
int3(0,-1,100),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_east(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-3,3,0),
|
||||
int3(-100,-1,0),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_south(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,3,-3),
|
||||
int3(0,-1,-100),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,-7)>>1,
|
||||
int3(-10,-1,1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_nw(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,-7)>>1,
|
||||
int3(10,-1,1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(7,6,0)>>1,
|
||||
int3(-1,-1,-10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(7,6,0)>>1,
|
||||
int3(-1,-1,10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,7)>>1,
|
||||
int3(-10,-1,-1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_sw(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,7)>>1,
|
||||
int3(10,-1,-1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-7,6,0)>>1,
|
||||
int3(1,-1,-10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-7,6,0)>>1,
|
||||
int3(1,-1,10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_oblique(){
|
||||
@ -2127,202 +2094,4 @@ mod test{
|
||||
Time::ZERO
|
||||
),None);
|
||||
}
|
||||
#[test]
|
||||
fn run_replay(){
|
||||
println!("loading map file..");
|
||||
let map=file::load("../tools/bhop_maps/5692113331.snfm");
|
||||
println!("loading bot file..");
|
||||
let bot=file::load("../tools/replays/534s+997497968ns.snfb");
|
||||
if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
println!("simulating...");
|
||||
let mut physics=PhysicsState::default();
|
||||
for ins in bot.instructions{
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
|
||||
}
|
||||
match physics.get_finish_time(){
|
||||
Some(time)=>println!("finish time:{}",time),
|
||||
None=>println!("simulation did not end in finished state"),
|
||||
}
|
||||
}else{
|
||||
panic!("missing files");
|
||||
}
|
||||
}
|
||||
enum DeterminismResult{
|
||||
Deterministic,
|
||||
NonDeterministic,
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum ReplayError{
|
||||
Load(file::LoadError),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
impl From<file::LoadError> for ReplayError{
|
||||
fn from(value:file::LoadError)->Self{
|
||||
Self::Load(value)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for ReplayError{
|
||||
fn from(value:std::io::Error)->Self{
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
|
||||
// create default physics state
|
||||
let mut physics_deterministic=PhysicsState::default();
|
||||
// create a second physics state
|
||||
let mut physics_filtered=PhysicsState::default();
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
println!("simulating...");
|
||||
|
||||
let mut non_idle_count=0;
|
||||
|
||||
for (i,ins) in bot.instructions.into_iter().enumerate(){
|
||||
let state_deterministic=physics_deterministic.clone();
|
||||
let state_filtered=physics_filtered.clone();
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
|
||||
match ins{
|
||||
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
|
||||
other=>{
|
||||
non_idle_count+=1;
|
||||
// run
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
|
||||
// check if position matches
|
||||
let b0=physics_deterministic.camera_body();
|
||||
let b1=physics_filtered.camera_body();
|
||||
if b0.position!=b1.position{
|
||||
println!("desync at instruction #{}",i);
|
||||
println!("non idle instructions completed={non_idle_count}");
|
||||
println!("instruction #{i}={:?}",other);
|
||||
println!("deterministic state0:\n{state_deterministic:?}");
|
||||
println!("filtered state0:\n{state_filtered:?}");
|
||||
println!("deterministic state1:\n{:?}",physics_deterministic);
|
||||
println!("filtered state1:\n{:?}",physics_filtered);
|
||||
return DeterminismResult::NonDeterministic;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
match physics_deterministic.get_finish_time(){
|
||||
Some(time)=>println!("[with idle] finish time:{}",time),
|
||||
None=>println!("[with idle] simulation did not end in finished state"),
|
||||
}
|
||||
match physics_filtered.get_finish_time(){
|
||||
Some(time)=>println!("[filtered] finish time:{}",time),
|
||||
None=>println!("[filtered] simulation did not end in finished state"),
|
||||
}
|
||||
DeterminismResult::Deterministic
|
||||
}
|
||||
type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>;
|
||||
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
|
||||
s.spawn(move ||{
|
||||
let result=match file::load(file_path.as_path()){
|
||||
Ok(file::LoadFormat::Bot(bot))=>{
|
||||
println!("Running {:?}",file_path.file_stem());
|
||||
Ok(Some(segment_determinism(bot,physics_data)))
|
||||
},
|
||||
Ok(_)=>{
|
||||
println!("Provided bot file is not a bot file!");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e)=>{
|
||||
println!("Load error");
|
||||
Err(e)
|
||||
},
|
||||
};
|
||||
// send when thread is complete
|
||||
send.send(result).unwrap();
|
||||
});
|
||||
}
|
||||
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
|
||||
Ok(dir_entry.file_type()?.is_file().then_some(
|
||||
dir_entry.path()
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_determinism()->Result<(),ReplayError>{
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
println!("loading map file..");
|
||||
let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{
|
||||
panic!("Provided map file is not a map file!");
|
||||
};
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let (send,recv)=std::sync::mpsc::channel();
|
||||
|
||||
let mut read_dir=std::fs::read_dir("../tools/replays")?;
|
||||
|
||||
// promise that &physics_data will outlive the spawned threads
|
||||
let thread_results=std::thread::scope(|s|{
|
||||
let mut thread_results=Vec::new();
|
||||
|
||||
// spawn threads
|
||||
println!("spawning up to {thread_limit} threads...");
|
||||
let mut active_thread_count=0;
|
||||
while active_thread_count<thread_limit{
|
||||
if let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
active_thread_count+=1;
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// spawn another thread every time a message is received from the channel
|
||||
println!("riding parallelism wave...");
|
||||
while let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
// wait for a thread to complete
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for remaining threads to complete
|
||||
println!("waiting for all threads to complete...");
|
||||
for _ in 0..active_thread_count{
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
}
|
||||
|
||||
println!("done.");
|
||||
Ok::<_,ReplayError>(thread_results)
|
||||
})?;
|
||||
|
||||
// tally results
|
||||
#[derive(Default)]
|
||||
struct Totals{
|
||||
deterministic:u32,
|
||||
nondeterministic:u32,
|
||||
invalid:u32,
|
||||
error:u32,
|
||||
}
|
||||
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
|
||||
match result{
|
||||
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
|
||||
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
|
||||
Ok(None)=>totals.invalid+=1,
|
||||
Err(_)=>totals.error+=1,
|
||||
}
|
||||
totals
|
||||
});
|
||||
|
||||
println!("deterministic={deterministic}");
|
||||
println!("nondeterministic={nondeterministic}");
|
||||
println!("invalid={invalid}");
|
||||
println!("error={error}");
|
||||
|
||||
assert!(nondeterministic==0);
|
||||
assert!(invalid==0);
|
||||
assert!(error==0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
|
||||
use strafesnet_common::integer::vec3::{self,Vector3};
|
||||
use strafesnet_common::integer::{Fixed,Planar64Vec3,Ratio};
|
||||
use strafesnet_common::ray::Ray;
|
||||
|
||||
// This algorithm is based on Lua code
|
||||
// written by Trey Reynolds in 2021
|
||||
@ -12,24 +14,6 @@ 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,
|
||||
@ -168,18 +152,19 @@ 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();
|
||||
//wrap for speed
|
||||
let direction=solve1(c0)?.divide().wrap_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();
|
||||
)?.divide().wrap_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 direction=solve2(c0,c1)?.divide().wrap_1();
|
||||
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
@ -187,11 +172,11 @@ fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
|
||||
let origin=point+solve2(
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
)?.divide().wrap_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 direction=solve3(c0,c1,c2)?.divide().wrap_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;
|
||||
@ -200,7 +185,7 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
&c2.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
)?.divide().wrap_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
|
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 = "2024"
|
||||
|
||||
[dependencies]
|
||||
glam = "0.30.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;
|
@ -5,13 +5,13 @@ use strafesnet_common::physics::{
|
||||
TimeInner as PhysicsTimeInner,
|
||||
Time as PhysicsTime,
|
||||
};
|
||||
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
|
||||
use strafesnet_common::session::Time as SessionTime;
|
||||
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
|
||||
|
||||
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
|
||||
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
|
||||
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTime>;
|
||||
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTime>;
|
||||
|
||||
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
|
||||
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTime>;
|
||||
|
||||
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
|
||||
|
||||
@ -89,14 +89,14 @@ pub struct MouseInterpolator{
|
||||
// Maybe MouseInterpolator manipulation is better expressed using impls
|
||||
// and called from Instruction trait impls in session
|
||||
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
|
||||
type TimeInner=SessionTimeInner;
|
||||
type Time=SessionTime;
|
||||
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>>{
|
||||
type Time=SessionTime;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{
|
||||
self.buffered_instruction_with_timeout(time_limit)
|
||||
}
|
||||
}
|
||||
@ -108,7 +108,7 @@ impl MouseInterpolator{
|
||||
output:std::collections::VecDeque::new(),
|
||||
}
|
||||
}
|
||||
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
|
||||
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTime>){
|
||||
self.buffer.push_front(TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:BufferedInstruction::Mouse(ins.instruction).into(),
|
||||
@ -219,7 +219,7 @@ impl MouseInterpolator{
|
||||
}
|
||||
}
|
||||
}
|
||||
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
|
||||
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTime>>{
|
||||
match self.get_mouse_timedout_at(time_limit){
|
||||
Some(timeout)=>Some(TimedInstruction{
|
||||
time:timeout,
|
||||
@ -232,7 +232,7 @@ impl MouseInterpolator{
|
||||
}),
|
||||
}
|
||||
}
|
||||
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
|
||||
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTime>)->Option<TimedPhysicsInstruction>{
|
||||
match ins.instruction{
|
||||
StepInstruction::Pop=>(),
|
||||
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
|
||||
@ -244,6 +244,7 @@ impl MouseInterpolator{
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use super::*;
|
||||
use strafesnet_common::session::TimeInner as SessionTimeInner;
|
||||
#[test]
|
||||
fn test(){
|
||||
let mut interpolator=MouseInterpolator::new();
|
@ -12,10 +12,11 @@ use strafesnet_common::physics::{
|
||||
};
|
||||
use strafesnet_common::timer::{Scaled,Timer};
|
||||
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
|
||||
use strafesnet_settings::directories::Directories;
|
||||
|
||||
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
|
||||
use crate::physics::{PhysicsContext,PhysicsData};
|
||||
use crate::settings::UserSettings;
|
||||
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
|
||||
use strafesnet_settings::settings::UserSettings;
|
||||
|
||||
pub enum Instruction<'a>{
|
||||
Input(SessionInputInstruction),
|
||||
@ -57,19 +58,19 @@ pub enum SessionPlaybackInstruction{
|
||||
}
|
||||
|
||||
pub struct FrameState{
|
||||
pub body:crate::physics::Body,
|
||||
pub camera:crate::physics::PhysicsCamera,
|
||||
pub body:physics::Body,
|
||||
pub camera:physics::PhysicsCamera,
|
||||
pub time:PhysicsTime,
|
||||
}
|
||||
|
||||
pub struct Simulation{
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:crate::physics::PhysicsState,
|
||||
physics:physics::PhysicsState,
|
||||
}
|
||||
impl Simulation{
|
||||
pub const fn new(
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:crate::physics::PhysicsState,
|
||||
physics:physics::PhysicsState,
|
||||
)->Self{
|
||||
Self{
|
||||
timer,
|
||||
@ -87,11 +88,11 @@ impl Simulation{
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Recording{
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTime>>,
|
||||
}
|
||||
impl Recording{
|
||||
pub fn new(
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTime>>,
|
||||
)->Self{
|
||||
Self{instructions}
|
||||
}
|
||||
@ -149,11 +150,12 @@ enum ViewState{
|
||||
}
|
||||
|
||||
pub struct Session{
|
||||
directories:Directories,
|
||||
user_settings:UserSettings,
|
||||
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
|
||||
view_state:ViewState,
|
||||
//gui:GuiState
|
||||
geometry_shared:crate::physics::PhysicsData,
|
||||
geometry_shared:physics::PhysicsData,
|
||||
simulation:Simulation,
|
||||
// below fields not included in lite session
|
||||
recording:Recording,
|
||||
@ -163,10 +165,12 @@ pub struct Session{
|
||||
impl Session{
|
||||
pub fn new(
|
||||
user_settings:UserSettings,
|
||||
directories:Directories,
|
||||
simulation:Simulation,
|
||||
)->Self{
|
||||
Self{
|
||||
user_settings,
|
||||
directories,
|
||||
mouse_interpolator:MouseInterpolator::new(),
|
||||
geometry_shared:Default::default(),
|
||||
simulation,
|
||||
@ -203,8 +207,8 @@ impl Session{
|
||||
// Session emits DoStep
|
||||
|
||||
impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
|
||||
type Time=SessionTime;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::Time>){
|
||||
// repetitive procedure macro
|
||||
macro_rules! run_mouse_interpolator_instruction{
|
||||
($instruction:expr)=>{
|
||||
@ -295,11 +299,17 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
let file_name=format!("replays/{}.snfb",ins.time);
|
||||
let mut replays_path=self.directories.replays.clone();
|
||||
let file_name=format!("{}.snfb",ins.time);
|
||||
std::thread::spawn(move ||{
|
||||
std::fs::create_dir_all("replays").unwrap();
|
||||
let file=std::fs::File::create(file_name).unwrap();
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),crate::physics::VERSION.get(),replay.recording.instructions).unwrap();
|
||||
std::fs::create_dir_all(replays_path.as_path()).unwrap();
|
||||
replays_path.push(file_name);
|
||||
let file=std::fs::File::create(replays_path).unwrap();
|
||||
strafesnet_snf::bot::write_bot(
|
||||
std::io::BufWriter::new(file),
|
||||
strafesnet_physics::VERSION.get(),
|
||||
replay.recording.instructions
|
||||
).unwrap();
|
||||
println!("Finished writing bot file!");
|
||||
});
|
||||
},
|
||||
@ -415,8 +425,8 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
}
|
||||
}
|
||||
impl InstructionConsumer<StepInstruction> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
|
||||
type Time=SessionTime;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::Time>){
|
||||
let time=self.simulation.timer.time(ins.time);
|
||||
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
|
||||
//record
|
||||
@ -426,8 +436,8 @@ impl InstructionConsumer<StepInstruction> for Session{
|
||||
}
|
||||
}
|
||||
impl InstructionEmitter<StepInstruction> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
|
||||
type Time=SessionTime;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{
|
||||
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 = "2024"
|
||||
|
||||
[dependencies]
|
||||
configparser = "3.0.2"
|
||||
directories = "6.0.0"
|
||||
glam = "0.30.0"
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
8
engine/settings/LICENSE
Normal file
8
engine/settings/LICENSE
Normal file
@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
32
engine/settings/src/directories.rs
Normal file
32
engine/settings/src/directories.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::settings::{UserSettings,load_user_settings};
|
||||
|
||||
pub struct Directories{
|
||||
pub settings:PathBuf,
|
||||
pub maps:PathBuf,
|
||||
pub replays:PathBuf,
|
||||
}
|
||||
impl Directories{
|
||||
pub fn settings(&self)->UserSettings{
|
||||
load_user_settings(&self.settings)
|
||||
}
|
||||
pub fn user()->Option<Self>{
|
||||
let dirs=directories::ProjectDirs::from("net.strafes","StrafesNET","Strafe Client")?;
|
||||
Some(Self{
|
||||
settings:dirs.config_dir().join("settings.conf"),
|
||||
maps:dirs.cache_dir().join("maps"),
|
||||
// separate directory for remote downloaded replays (cache)
|
||||
// bots:dirs.cache_dir().join("bots"),
|
||||
replays:dirs.data_local_dir().join("replays"),
|
||||
})
|
||||
}
|
||||
pub fn portable()->Result<Self,std::io::Error>{
|
||||
let current_dir=std::env::current_dir()?;
|
||||
Ok(Self{
|
||||
settings:current_dir.join("settings.conf"),
|
||||
maps:current_dir.join("maps"),
|
||||
replays:current_dir.join("replays"),
|
||||
})
|
||||
}
|
||||
}
|
2
engine/settings/src/lib.rs
Normal file
2
engine/settings/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod settings;
|
||||
pub mod directories;
|
@ -74,9 +74,9 @@ sensitivity_y_from_x_ratio=1
|
||||
Sensitivity::DeriveY{x:0.0.001,y:DerivedSensitivity{ratio:1.0}}
|
||||
*/
|
||||
|
||||
pub fn read_user_settings()->UserSettings{
|
||||
pub fn load_user_settings(path:&std::path::Path)->UserSettings{
|
||||
let mut cfg=configparser::ini::Ini::new();
|
||||
if let Ok(_)=cfg.load("settings.conf"){
|
||||
if let Ok(_)=cfg.load(path){
|
||||
let (cfg_fov_x,cfg_fov_y)=(cfg.getfloat("camera","fov_x"),cfg.getfloat("camera","fov_y"));
|
||||
let fov=match(cfg_fov_x,cfg_fov_y){
|
||||
(Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly {
|
||||
@ -136,4 +136,4 @@ pub fn read_user_settings()->UserSettings{
|
||||
}else{
|
||||
UserSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
9
integration-testing/Cargo.toml
Normal file
9
integration-testing/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "integration-testing"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
|
||||
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
|
||||
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }
|
253
integration-testing/src/main.rs
Normal file
253
integration-testing/src/main.rs
Normal file
@ -0,0 +1,253 @@
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
|
||||
|
||||
fn main(){
|
||||
let arg=std::env::args().skip(1).next();
|
||||
match arg.as_deref(){
|
||||
Some("determinism")|None=>test_determinism().unwrap(),
|
||||
Some("replay")=>run_replay().unwrap(),
|
||||
_=>println!("invalid argument"),
|
||||
}
|
||||
}
|
||||
|
||||
#[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 data=std::fs::read(path)?;
|
||||
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,
|
||||
}
|
||||
struct SegmentResult{
|
||||
determinism:DeterminismResult,
|
||||
ticks:u64,
|
||||
nanoseconds:u64,
|
||||
}
|
||||
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->SegmentResult{
|
||||
// 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;
|
||||
let instruction_count=bot.instructions.len();
|
||||
|
||||
let start=Instant::now();
|
||||
|
||||
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{
|
||||
let nanoseconds=start.elapsed().as_nanos() as u64;
|
||||
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 SegmentResult{
|
||||
determinism:DeterminismResult::NonDeterministic,
|
||||
ticks:1+i as u64+non_idle_count,
|
||||
nanoseconds,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
let nanoseconds=start.elapsed().as_nanos() as u64;
|
||||
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"),
|
||||
}
|
||||
SegmentResult{
|
||||
determinism:DeterminismResult::Deterministic,
|
||||
ticks:instruction_count as u64+non_idle_count,
|
||||
nanoseconds,
|
||||
}
|
||||
|
||||
}
|
||||
type ThreadResult=Result<Option<SegmentResult>,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 mut nanoseconds=0;
|
||||
let mut ticks=0;
|
||||
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
|
||||
match result{
|
||||
Ok(Some(segment_result))=>{
|
||||
ticks+=segment_result.ticks;
|
||||
nanoseconds+=segment_result.nanoseconds;
|
||||
match segment_result.determinism{
|
||||
DeterminismResult::Deterministic=>totals.deterministic+=1,
|
||||
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}");
|
||||
println!("average ticks/s per core: {}",ticks*1_000_000_000/nanoseconds);
|
||||
|
||||
assert!(nondeterministic==0);
|
||||
assert!(invalid==0);
|
||||
assert!(error==0);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "strafesnet_bsp_loader"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Convert Valve BSP files to StrafesNET data structures."
|
||||
@ -10,7 +10,10 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
glam = "0.29.0"
|
||||
strafesnet_common = { path = "../common", registry = "strafesnet" }
|
||||
vbsp = "0.6.0"
|
||||
glam = "0.30.0"
|
||||
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
|
||||
vbsp = "0.9.1"
|
||||
vbsp-entities-css = "0.6.0"
|
||||
vmdl = "0.2.0"
|
||||
vpk = "0.3.0"
|
||||
|
343
lib/bsp_loader/src/brush.rs
Normal file
343
lib/bsp_loader/src/brush.rs
Normal file
@ -0,0 +1,343 @@
|
||||
use strafesnet_common::integer::Planar64;
|
||||
use strafesnet_common::{model,integer};
|
||||
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
|
||||
|
||||
use crate::{valve_transform_normal,valve_transform_dist};
|
||||
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
struct Face{
|
||||
normal:integer::Planar64Vec3,
|
||||
dot:integer::Planar64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Faces{
|
||||
faces:Vec<Vec<integer::Planar64Vec3>>,
|
||||
}
|
||||
|
||||
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<3,96>>>{
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=c2.normal.dot(n0_n1);
|
||||
if det.abs().is_zero(){
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
c1.normal.cross(c2.normal)*c0.dot
|
||||
+c2.normal.cross(c0.normal)*c1.dot
|
||||
+c0.normal.cross(c1.normal)*c2.dot
|
||||
)/det)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PlanesToFacesError{
|
||||
InitFace1,
|
||||
InitFace2,
|
||||
InitIntersection,
|
||||
FindNewIntersection,
|
||||
// Narrow(strafesnet_common::integer::NarrowError),
|
||||
EmptyFaces,
|
||||
InfiniteLoop1,
|
||||
InfiniteLoop2,
|
||||
}
|
||||
impl std::fmt::Display for PlanesToFacesError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl core::error::Error for PlanesToFacesError{}
|
||||
|
||||
fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,PlanesToFacesError>{
|
||||
let mut faces=Vec::new();
|
||||
// for each face, determine one edge at a time until you complete the face
|
||||
'face: for face0 in &face_list{
|
||||
// 1. find first edge
|
||||
// 2. follow edges around face
|
||||
|
||||
// === finding first edge ===
|
||||
// 1. pick the most perpendicular set of 3 faces
|
||||
// 2. check if any faces occlude the intersection
|
||||
// 3. use this test to replace left and right alternating until they are not occluded
|
||||
|
||||
// find the most perpendicular face to face0
|
||||
let mut face1=face_list.iter().min_by_key(|&p|{
|
||||
face0.normal.dot(p.normal).abs()
|
||||
}).ok_or(PlanesToFacesError::InitFace1)?;
|
||||
|
||||
// direction of edge formed by face0 x face1
|
||||
let edge_dir=face0.normal.cross(face1.normal);
|
||||
|
||||
// find the most perpendicular face to both face0 and face1
|
||||
let mut face2=face_list.iter().max_by_key(|&p|{
|
||||
// find the best *oriented* face (no .abs())
|
||||
edge_dir.dot(p.normal)
|
||||
}).ok_or(PlanesToFacesError::InitFace2)?;
|
||||
|
||||
let mut detect_loop=200u8;
|
||||
|
||||
let mut intersection=solve3(face0,face1,face2).ok_or(PlanesToFacesError::InitIntersection)?;
|
||||
|
||||
// repeatedly update face1, face2 until all faces form part of the convex solid
|
||||
'find: loop{
|
||||
detect_loop=detect_loop.checked_sub(1).ok_or(PlanesToFacesError::InfiniteLoop1)?;
|
||||
// test if any *other* faces occlude the intersection
|
||||
for new_face in &face_list{
|
||||
// new face occludes intersection point
|
||||
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
|
||||
// replace one of the faces with the new face
|
||||
// dont' try to replace face0 because we are exploring that face in particular
|
||||
if let Some(new_intersection)=solve3(face0,new_face,face2){
|
||||
// face1 does not occlude (or intersect) the new intersection
|
||||
if (face1.dot.widen_2()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
|
||||
face1=new_face;
|
||||
intersection=new_intersection;
|
||||
continue 'find;
|
||||
}
|
||||
}
|
||||
if let Some(new_intersection)=solve3(face0,face1,new_face){
|
||||
// face2 does not occlude (or intersect) the new intersection
|
||||
if (face2.dot.widen_2()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
|
||||
face2=new_face;
|
||||
intersection=new_intersection;
|
||||
continue 'find;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we have found a set of faces for which the intersection is on the convex solid
|
||||
break 'find;
|
||||
}
|
||||
|
||||
// check if face0 must go, meaning it is a degenerate face and does not contribute anything to the convex solid
|
||||
for new_face in &face_list{
|
||||
if core::ptr::eq(face0,new_face){
|
||||
continue;
|
||||
}
|
||||
if core::ptr::eq(face1,new_face){
|
||||
continue;
|
||||
}
|
||||
if core::ptr::eq(face2,new_face){
|
||||
continue;
|
||||
}
|
||||
// new_face occludes intersection meaning intersection is not on convex solid and face0 is degenrate
|
||||
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
|
||||
// abort! reject face0 entirely
|
||||
continue 'face;
|
||||
}
|
||||
}
|
||||
|
||||
// === follow edges around face ===
|
||||
// Note that we chose face2 such that the 3 faces create a particular winding order.
|
||||
// If we choose a consistent face to follow (face1, face2) it will always wind with a consistent chirality
|
||||
|
||||
let mut detect_loop=200u8;
|
||||
|
||||
// keep looping until we meet this face again
|
||||
let face1=face1;
|
||||
let mut face=Vec::new();
|
||||
loop{
|
||||
// push point onto vertices
|
||||
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
|
||||
face.push(intersection.divide().narrow_1().unwrap());
|
||||
|
||||
// we looped back around to face1, we're done!
|
||||
if core::ptr::eq(face1,face2){
|
||||
break;
|
||||
}
|
||||
|
||||
// the measure
|
||||
let edge_dir=face0.normal.cross(face2.normal);
|
||||
|
||||
// the dot product to beat
|
||||
let d_intersection=edge_dir.dot(intersection.num)/intersection.den;
|
||||
|
||||
// find the next face moving clockwise around face0
|
||||
let (new_face,new_intersection,_)=face_list.iter().filter_map(|new_face|{
|
||||
// ignore faces that are part of the current edge
|
||||
if core::ptr::eq(face0,new_face)
|
||||
|core::ptr::eq(face2,new_face){
|
||||
return None;
|
||||
}
|
||||
let new_intersection=solve3(face0,face2,new_face)?;
|
||||
|
||||
// the d value must be larger
|
||||
let d_new_intersection=edge_dir.dot(new_intersection.num)/new_intersection.den;
|
||||
if d_new_intersection.le_ratio(d_intersection){
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((new_face,new_intersection,d_new_intersection))
|
||||
}).min_by_key(|&(_,_,d)|d).ok_or(PlanesToFacesError::FindNewIntersection)?;
|
||||
|
||||
face2=new_face;
|
||||
intersection=new_intersection;
|
||||
|
||||
detect_loop=detect_loop.checked_sub(1).ok_or(PlanesToFacesError::InfiniteLoop2)?;
|
||||
}
|
||||
|
||||
faces.push(face);
|
||||
}
|
||||
|
||||
if faces.is_empty(){
|
||||
Err(PlanesToFacesError::EmptyFaces)
|
||||
}else{
|
||||
Ok(Faces{
|
||||
faces,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum BrushToMeshError{
|
||||
SliceBrushSides,
|
||||
MissingPlane,
|
||||
InvalidFaceCount{
|
||||
count:usize,
|
||||
},
|
||||
InvalidPlanes(PlanesToFacesError),
|
||||
}
|
||||
impl std::fmt::Display for BrushToMeshError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl core::error::Error for BrushToMeshError{}
|
||||
|
||||
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
|
||||
// generate the mesh
|
||||
let mut mb=model::MeshBuilder::new();
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
|
||||
// normals are ignored by physics
|
||||
let normal=mb.acquire_normal_id(integer::vec3::ZERO);
|
||||
|
||||
let polygon_list=faces.into_iter().map(|face|{
|
||||
face.into_iter().map(|pos|{
|
||||
let pos=mb.acquire_pos_id(pos);
|
||||
mb.acquire_vertex_id(model::IndexedVertex{
|
||||
pos,
|
||||
tex,
|
||||
normal,
|
||||
color,
|
||||
})
|
||||
}).collect()
|
||||
}).collect();
|
||||
|
||||
let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))];
|
||||
let physics_groups=vec![model::IndexedPhysicsGroup{
|
||||
groups:vec![model::PolygonGroupId::new(0)],
|
||||
}];
|
||||
let graphics_groups=vec![];
|
||||
|
||||
mb.build(polygon_groups,graphics_groups,physics_groups)
|
||||
}
|
||||
|
||||
pub fn brush_to_mesh(bsp:&vbsp::Bsp,brush:&vbsp::Brush)->Result<model::Mesh,BrushToMeshError>{
|
||||
let brush_start_idx=brush.brush_side as usize;
|
||||
let sides_range=brush_start_idx..brush_start_idx+brush.num_brush_sides as usize;
|
||||
let sides=bsp.brush_sides.get(sides_range).ok_or(BrushToMeshError::SliceBrushSides)?;
|
||||
let face_list=sides.iter().map(|side|{
|
||||
// The so-called tumor brushes have TRIGGER bit set
|
||||
// but also ignore visleaf hint and skip sides
|
||||
const TUMOR:vbsp::TextureFlags=vbsp::TextureFlags::HINT.union(vbsp::TextureFlags::SKIP).union(vbsp::TextureFlags::TRIGGER);
|
||||
if let Some(texture_info)=bsp.texture_info(side.texture_info as usize){
|
||||
if texture_info.flags.intersects(TUMOR){
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let plane=bsp.plane(side.plane as usize)?;
|
||||
Some(Face{
|
||||
normal:valve_transform_normal(plane.normal.into()),
|
||||
dot:valve_transform_dist(plane.dist.into()),
|
||||
})
|
||||
}).collect::<Option<std::collections::HashSet<_>>>().ok_or(BrushToMeshError::MissingPlane)?;
|
||||
|
||||
if face_list.len()<4{
|
||||
return Err(BrushToMeshError::InvalidFaceCount{count:face_list.len()});
|
||||
}
|
||||
|
||||
let faces=planes_to_faces(face_list).map_err(BrushToMeshError::InvalidPlanes)?;
|
||||
|
||||
let mesh=faces_to_mesh(faces.faces);
|
||||
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
pub fn unit_cube()->model::Mesh{
|
||||
let face_list=[
|
||||
Face{normal:integer::vec3::X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
|
||||
].into_iter().collect();
|
||||
let faces=planes_to_faces(face_list).unwrap();
|
||||
let mesh=faces_to_mesh(faces.faces);
|
||||
mesh
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_cube(){
|
||||
let face_list=[
|
||||
Face{normal:integer::vec3::X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
|
||||
].into_iter().collect();
|
||||
let faces=planes_to_faces(face_list).unwrap();
|
||||
assert_eq!(faces.faces.len(),6);
|
||||
dbg!(faces);
|
||||
}
|
||||
#[test]
|
||||
fn test_cube_with_degernate_face(){
|
||||
let face_list=[
|
||||
Face{normal:integer::vec3::X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Z,dot:Planar64::EPSILON},
|
||||
].into_iter().collect();
|
||||
let faces=planes_to_faces(face_list).unwrap();
|
||||
assert_eq!(faces.faces.len(),6);
|
||||
dbg!(faces);
|
||||
}
|
||||
#[test]
|
||||
fn test_cube_with_degernate_face2(){
|
||||
let face_list=[
|
||||
Face{normal:integer::vec3::X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X+integer::vec3::NEG_Z,dot:-Planar64::EPSILON},
|
||||
].into_iter().collect();
|
||||
let faces=planes_to_faces(face_list).unwrap();
|
||||
assert_eq!(faces.faces.len(),5);
|
||||
dbg!(faces);
|
||||
}
|
||||
#[test]
|
||||
fn test_cube_with_degernate_face3(){
|
||||
let face_list=[
|
||||
Face{normal:integer::vec3::X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
|
||||
Face{normal:integer::vec3::NEG_X+integer::vec3::NEG_Z,dot:Planar64::EPSILON},
|
||||
].into_iter().collect();
|
||||
let faces=planes_to_faces(face_list).unwrap();
|
||||
assert_eq!(faces.faces.len(),7);
|
||||
dbg!(faces);
|
||||
}
|
||||
}
|
@ -1,43 +1,142 @@
|
||||
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 vbsp_entities_css::Entity;
|
||||
|
||||
use strafesnet_common::{map,model,integer,gameplay_attributes as attr};
|
||||
use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
use strafesnet_deferred_loader::mesh::Meshes;
|
||||
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
|
||||
use strafesnet_common::gameplay_modes::{NormalizedMode,NormalizedModes,Mode,Stage};
|
||||
|
||||
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,
|
||||
{
|
||||
//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;
|
||||
fn add_brush<'a>(
|
||||
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
|
||||
world_models:&mut Vec<model::Model>,
|
||||
prop_models:&mut Vec<model::Model>,
|
||||
model:&'a str,
|
||||
origin:vbsp::Vector,
|
||||
rendercolor:vbsp::Color,
|
||||
attributes:attr::CollisionAttributesId,
|
||||
){
|
||||
let transform=integer::Planar64Affine3::from_translation(
|
||||
valve_transform(origin.into())
|
||||
);
|
||||
let color=(glam::Vec3::from_array([
|
||||
rendercolor.r as f32,
|
||||
rendercolor.g as f32,
|
||||
rendercolor.b as f32
|
||||
])/255.0).extend(1.0);
|
||||
|
||||
match model.chars().next(){
|
||||
// The first character of brush.model is '*'
|
||||
Some('*')=>match model[1..].parse(){
|
||||
Ok(mesh_id)=>{
|
||||
let mesh=model::MeshId::new(mesh_id);
|
||||
world_models.push(
|
||||
model::Model{mesh,attributes,transform,color}
|
||||
);
|
||||
},
|
||||
Err(e)=>{
|
||||
println!("Brush model int parse error: {e} model={model}");
|
||||
return;
|
||||
},
|
||||
},
|
||||
_=>{
|
||||
let mesh=mesh_deferred_loader.acquire_mesh_id(model);
|
||||
prop_models.push(
|
||||
model::Model{mesh,attributes,transform,color}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 unique_attributes=vec![
|
||||
attr::CollisionAttributes::Decoration,
|
||||
attr::CollisionAttributes::contact_default(),
|
||||
attr::CollisionAttributes::intersect_default(),
|
||||
// ladder
|
||||
attr::CollisionAttributes::Contact(
|
||||
attr::ContactAttributes{
|
||||
contacting:attr::ContactingAttributes{
|
||||
contact_behaviour:Some(attr::ContactingBehaviour::Ladder(
|
||||
attr::ContactingLadder{sticky:true}
|
||||
))
|
||||
},
|
||||
general:attr::GeneralAttributes::default(),
|
||||
}
|
||||
),
|
||||
// water
|
||||
attr::CollisionAttributes::Intersect(
|
||||
attr::IntersectAttributes{
|
||||
intersecting:attr::IntersectingAttributes{
|
||||
water:Some(attr::IntersectingWater{
|
||||
viscosity:integer::Planar64::ONE,
|
||||
density:integer::Planar64::ONE,
|
||||
velocity:integer::vec3::ZERO,
|
||||
}),
|
||||
},
|
||||
general:attr::GeneralAttributes::default(),
|
||||
}
|
||||
),
|
||||
];
|
||||
const ATTRIBUTE_DECORATION:attr::CollisionAttributesId=attr::CollisionAttributesId::new(0);
|
||||
const ATTRIBUTE_CONTACT_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(1);
|
||||
const ATTRIBUTE_INTERSECT_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(2);
|
||||
const ATTRIBUTE_LADDER_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(3);
|
||||
const ATTRIBUTE_WATER_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(4);
|
||||
|
||||
//declare all prop models to Loader
|
||||
let prop_models=bsp.static_props().map(|prop|{
|
||||
let mut prop_models=bsp.static_props().map(|prop|{
|
||||
const DEG_TO_RAD:f32=std::f32::consts::TAU/360.0;
|
||||
//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 placement=prop.as_prop_placement();
|
||||
let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
|
||||
model::Model{
|
||||
mesh:mesh_id,
|
||||
attributes:TEMP_TOUCH_ME_ATTRIBUTE,
|
||||
attributes:ATTRIBUTE_DECORATION,
|
||||
transform:integer::Planar64Affine3::new(
|
||||
integer::mat3::try_from_f32_array_2d((
|
||||
glam::Mat3A::from_diagonal(glam::Vec3::splat(placement.scale))
|
||||
integer::mat3::try_from_f32_array_2d(
|
||||
//TODO: figure this out
|
||||
*glam::Mat3A::from_quat(glam::Quat::from_array(placement.rotation.into()))
|
||||
).to_cols_array_2d()).unwrap(),
|
||||
valve_transform(placement.origin.into()),
|
||||
glam::Mat3A::from_euler(
|
||||
glam::EulerRot::XYZ,
|
||||
prop.angles.pitch*DEG_TO_RAD,
|
||||
prop.angles.yaw*DEG_TO_RAD,
|
||||
prop.angles.roll*DEG_TO_RAD
|
||||
).to_cols_array_2d()
|
||||
).unwrap(),
|
||||
valve_transform(prop.origin.into()),
|
||||
),
|
||||
color:glam::Vec4::ONE,
|
||||
}
|
||||
@ -47,14 +146,12 @@ 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 world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
|
||||
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,139 +160,200 @@ 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
|
||||
graphics_groups.push(model::IndexedGraphicsGroup{
|
||||
render:render_id,
|
||||
groups:vec![polygon_group_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![],
|
||||
});
|
||||
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>=
|
||||
//one instance of the main world mesh
|
||||
std::iter::once((
|
||||
//world_model
|
||||
model::MeshId::new(0),
|
||||
//model_origin
|
||||
vbsp::Vector::from([0.0,0.0,0.0]),
|
||||
//model_color
|
||||
vbsp::Color{r:255,g:255,b:255},
|
||||
)).chain(
|
||||
//entities sprinkle instances of the other meshes around
|
||||
bsp.entities.iter()
|
||||
.flat_map(|ent|ent.parse())//ignore entity parsing errors
|
||||
.filter_map(|ent|match ent{
|
||||
vbsp::Entity::Brush(brush)=>Some(brush),
|
||||
vbsp::Entity::BrushIllusionary(brush)=>Some(brush),
|
||||
vbsp::Entity::BrushWall(brush)=>Some(brush),
|
||||
vbsp::Entity::BrushWallToggle(brush)=>Some(brush),
|
||||
_=>None,
|
||||
}).flat_map(|brush|
|
||||
//The first character of brush.model is '*'
|
||||
brush.model[1..].parse().map(|mesh_id|//ignore parse int errors
|
||||
(model::MeshId::new(mesh_id),brush.origin,brush.color)
|
||||
)
|
||||
)
|
||||
).map(|(mesh_id,model_origin,vbsp::Color{r,g,b})|{
|
||||
model::Model{
|
||||
mesh:mesh_id,
|
||||
attributes:TEMP_TOUCH_ME_ATTRIBUTE,
|
||||
transform:integer::Planar64Affine3::new(
|
||||
integer::mat3::identity(),
|
||||
valve_transform(model_origin.into())
|
||||
),
|
||||
color:(glam::Vec3::from_array([r as f32,g as f32,b as f32])/255.0).extend(1.0),
|
||||
let mut found_spawn=None;
|
||||
|
||||
let mut world_models=Vec::new();
|
||||
|
||||
// the one and only world model 0
|
||||
world_models.push(model::Model{
|
||||
mesh:model::MeshId::new(0),
|
||||
attributes:ATTRIBUTE_DECORATION,
|
||||
transform:integer::Planar64Affine3::IDENTITY,
|
||||
color:glam::Vec4::W,
|
||||
});
|
||||
|
||||
const WHITE:vbsp::Color=vbsp::Color{r:255,g:255,b:255};
|
||||
for raw_ent in &bsp.entities{
|
||||
match raw_ent.parse(){
|
||||
Ok(Entity::Cycler(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::EnvSprite(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncBreakable(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncBrush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncDoor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncIllusionary(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncMonitor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncMovelinear(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncPhysbox(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncPhysboxMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncRotButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncTracktrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncTrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncWall(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncWallToggle(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::FuncWaterAnalog(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.unwrap_or(WHITE),ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropDynamic(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropDynamicOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropPhysics(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropPhysicsMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropPhysicsOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::PropRagdoll(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerGravity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerHurt(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerLook(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerMultiple(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerOnce(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerProximity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerPush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerSoundscape(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerTeleport(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerVphysicsMotion(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::TriggerWind(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
|
||||
Ok(Entity::InfoPlayerCounterterrorist(spawn))=>{
|
||||
found_spawn=Some(valve_transform(spawn.origin.into()));
|
||||
},
|
||||
Ok(Entity::InfoPlayerTerrorist(spawn))=>{
|
||||
found_spawn=Some(valve_transform(spawn.origin.into()));
|
||||
},
|
||||
Err(e)=>{
|
||||
println!("Bsp Entity parse error: {e}");
|
||||
},
|
||||
_=>(),
|
||||
}
|
||||
}).collect();
|
||||
}
|
||||
|
||||
// physics models
|
||||
for brush in &bsp.brushes{
|
||||
const RELEVANT:vbsp::BrushFlags=
|
||||
vbsp::BrushFlags::SOLID
|
||||
.union(vbsp::BrushFlags::PLAYERCLIP)
|
||||
.union(vbsp::BrushFlags::WATER)
|
||||
.union(vbsp::BrushFlags::MOVEABLE)
|
||||
.union(vbsp::BrushFlags::LADDER);
|
||||
if !brush.flags.intersects(RELEVANT){
|
||||
continue;
|
||||
}
|
||||
let is_ladder=brush.flags.contains(vbsp::BrushFlags::LADDER);
|
||||
let is_water=brush.flags.contains(vbsp::BrushFlags::WATER);
|
||||
let attributes=match (is_ladder,is_water){
|
||||
(true,false)=>ATTRIBUTE_LADDER_DEFAULT,
|
||||
(false,true)=>ATTRIBUTE_WATER_DEFAULT,
|
||||
(false,false)=>ATTRIBUTE_CONTACT_DEFAULT,
|
||||
(true,true)=>{
|
||||
// water ladder? wtf
|
||||
println!("brush is a water ladder o_o defaulting to ladder");
|
||||
ATTRIBUTE_LADDER_DEFAULT
|
||||
}
|
||||
};
|
||||
let mesh_result=crate::brush::brush_to_mesh(bsp,brush);
|
||||
match mesh_result{
|
||||
Ok(mesh)=>{
|
||||
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
|
||||
world_meshes.push(mesh);
|
||||
world_models.push(model::Model{
|
||||
mesh:mesh_id,
|
||||
attributes,
|
||||
transform:integer::Planar64Affine3::new(
|
||||
integer::mat3::identity(),
|
||||
integer::vec3::ZERO,
|
||||
),
|
||||
color:glam::Vec4::ONE,
|
||||
});
|
||||
},
|
||||
Err(e)=>println!("Brush mesh error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
let mut modes_list=Vec::new();
|
||||
if let Some(spawn_point)=found_spawn{
|
||||
// create a new mesh
|
||||
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
|
||||
world_meshes.push(crate::brush::unit_cube());
|
||||
// create a new model
|
||||
let model_id=model::ModelId::new(world_models.len() as u32);
|
||||
world_models.push(model::Model{
|
||||
mesh:mesh_id,
|
||||
attributes:ATTRIBUTE_INTERSECT_DEFAULT,
|
||||
transform:integer::Planar64Affine3::from_translation(spawn_point),
|
||||
color:glam::Vec4::W,
|
||||
});
|
||||
|
||||
let first_stage=Stage::empty(model_id);
|
||||
let main_mode=Mode::new(
|
||||
strafesnet_common::gameplay_style::StyleModifiers::source_bhop(),
|
||||
model_id,
|
||||
std::collections::HashMap::new(),
|
||||
vec![first_stage],
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
modes_list.push(NormalizedMode::new(main_mode));
|
||||
}
|
||||
|
||||
PartialMap1{
|
||||
attributes:unique_attributes,
|
||||
world_meshes,
|
||||
prop_models,
|
||||
world_models,
|
||||
modes:strafesnet_common::gameplay_modes::Modes::new(Vec::new()),
|
||||
modes:NormalizedModes::new(modes_list),
|
||||
}
|
||||
}
|
||||
|
||||
//partially constructed map types
|
||||
pub struct PartialMap1{
|
||||
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
|
||||
attributes:Vec<attr::CollisionAttributes>,
|
||||
prop_models:Vec<model::Model>,
|
||||
world_meshes:Vec<model::Mesh>,
|
||||
world_models:Vec<model::Model>,
|
||||
modes:strafesnet_common::gameplay_modes::Modes,
|
||||
modes:NormalizedModes,
|
||||
}
|
||||
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,
|
||||
@ -204,18 +362,17 @@ impl PartialMap1{
|
||||
}
|
||||
}
|
||||
pub struct PartialMap2{
|
||||
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
|
||||
attributes:Vec<attr::CollisionAttributes>,
|
||||
prop_meshes:Vec<(model::MeshId,model::Mesh)>,
|
||||
prop_models:Vec<model::Model>,
|
||||
world_meshes:Vec<model::Mesh>,
|
||||
world_models:Vec<model::Model>,
|
||||
modes:strafesnet_common::gameplay_modes::Modes,
|
||||
modes:NormalizedModes,
|
||||
}
|
||||
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 +391,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 +415,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,20 @@
|
||||
mod bsp;
|
||||
pub mod data;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
pub use data::Bsp;
|
||||
mod bsp;
|
||||
mod mesh;
|
||||
mod brush;
|
||||
pub mod loader;
|
||||
|
||||
const VALVE_SCALE:f32=1.0/16.0;
|
||||
pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{
|
||||
(d*VALVE_SCALE).try_into().unwrap()
|
||||
}
|
||||
pub(crate) fn valve_transform_normal([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
|
||||
strafesnet_common::integer::vec3::try_from_f32_array([x,z,-y]).unwrap()
|
||||
}
|
||||
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 +28,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 +68,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)
|
||||
}
|
||||
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();
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
165
lib/bsp_loader/src/loader.rs
Normal file
165
lib/bsp_loader/src/loader.rs
Normal file
@ -0,0 +1,165 @@
|
||||
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;
|
||||
impl TextureLoader{
|
||||
pub fn new()->Self{
|
||||
Self
|
||||
}
|
||||
}
|
||||
impl Loader for TextureLoader{
|
||||
type Error=TextureError;
|
||||
type Index<'a>=Cow<'a,str>;
|
||||
type Resource=Texture;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->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(String),
|
||||
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>{
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
}
|
||||
impl ModelLoader<'_,'_>{
|
||||
#[inline]
|
||||
pub const fn new<'bsp,'vpk>(
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
)->ModelLoader<'bsp,'vpk>{
|
||||
ModelLoader{
|
||||
finder,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=&'a str where Self:'a;
|
||||
type Resource=vmdl::Model;
|
||||
fn load<'a>(&'a mut self,index:Self::Index<'a>)->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(mdl_path_lower))?;
|
||||
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,'str>{
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>,
|
||||
}
|
||||
impl MeshLoader<'_,'_,'_,'_>{
|
||||
#[inline]
|
||||
pub const fn new<'bsp,'vpk,'load,'str>(
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>,
|
||||
)->MeshLoader<'bsp,'vpk,'load,'str>{
|
||||
MeshLoader{
|
||||
finder,
|
||||
deferred_loader
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'str,'bsp,'vpk,'load> Loader for MeshLoader<'bsp,'vpk,'load,'str>{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=&'a str where Self:'a;
|
||||
type Resource=Mesh;
|
||||
fn load<'a>(&'a mut self,index:Self::Index<'a>)->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,7 +1,7 @@
|
||||
[package]
|
||||
name = "strafesnet_common"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Common types and helpers for Strafe Client associated projects."
|
||||
@ -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" }
|
||||
glam = "0.29.0"
|
||||
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
|
||||
linear_ops = { version = "0.1.1", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
|
||||
glam = "0.30.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
|
@ -7,42 +7,57 @@ pub struct Aabb{
|
||||
}
|
||||
|
||||
impl Default for Aabb{
|
||||
#[inline]
|
||||
fn default()->Self{
|
||||
Self{min:vec3::MAX,max:vec3::MIN}
|
||||
}
|
||||
}
|
||||
|
||||
impl Aabb{
|
||||
#[inline]
|
||||
pub const fn new(min:Planar64Vec3,max:Planar64Vec3)->Self{
|
||||
Self{min,max}
|
||||
}
|
||||
#[inline]
|
||||
pub const fn max(&self)->Planar64Vec3{
|
||||
self.max
|
||||
}
|
||||
#[inline]
|
||||
pub const fn min(&self)->Planar64Vec3{
|
||||
self.min
|
||||
}
|
||||
#[inline]
|
||||
pub fn grow(&mut self,point:Planar64Vec3){
|
||||
self.min=self.min.min(point);
|
||||
self.max=self.max.max(point);
|
||||
}
|
||||
#[inline]
|
||||
pub fn join(&mut self,aabb:&Aabb){
|
||||
self.min=self.min.min(aabb.min);
|
||||
self.max=self.max.max(aabb.max);
|
||||
}
|
||||
#[inline]
|
||||
pub fn inflate(&mut self,hs:Planar64Vec3){
|
||||
self.min-=hs;
|
||||
self.max+=hs;
|
||||
}
|
||||
#[inline]
|
||||
pub fn contains(&self,point:Planar64Vec3)->bool{
|
||||
let bvec=self.min.lt(point)&point.lt(self.max);
|
||||
bvec.all()
|
||||
}
|
||||
#[inline]
|
||||
pub fn intersects(&self,aabb:&Aabb)->bool{
|
||||
let bvec=self.min.lt(aabb.max)&aabb.min.lt(self.max);
|
||||
bvec.all()
|
||||
}
|
||||
#[inline]
|
||||
pub fn size(&self)->Planar64Vec3{
|
||||
self.max-self.min
|
||||
}
|
||||
#[inline]
|
||||
pub fn center(&self)->Planar64Vec3{
|
||||
self.min+(self.max-self.min)>>1
|
||||
self.min.map_zip(self.max,|(min,max)|min.midpoint(max))
|
||||
}
|
||||
//probably use floats for area & volume because we don't care about precision
|
||||
// pub fn area_weight(&self)->f32{
|
||||
|
@ -1,4 +1,10 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::aabb::Aabb;
|
||||
use crate::ray::Ray;
|
||||
use crate::integer::{Ratio,Planar64};
|
||||
use crate::instruction::{InstructionCollector,TimedInstruction};
|
||||
|
||||
//da algaritum
|
||||
//lista boxens
|
||||
@ -10,35 +16,117 @@ use crate::aabb::Aabb;
|
||||
//sort the centerpoints on each axis (3 lists)
|
||||
//bv is put into octant based on whether it is upper or lower in each list
|
||||
|
||||
pub enum RecursiveContent<R,T>{
|
||||
Branch(Vec<R>),
|
||||
Leaf(T),
|
||||
|
||||
pub fn intersect_aabb(ray:&Ray,aabb:&Aabb)->Option<Ratio<Planar64,Planar64>>{
|
||||
// n.(o+d*t)==n.p
|
||||
// n.o + n.d * t == n.p
|
||||
// t == (n.p - n.o)/n.d
|
||||
let mut hit=None;
|
||||
match ray.direction.x.cmp(&Planar64::ZERO){
|
||||
Ordering::Less=>{
|
||||
let rel_min=aabb.min()-ray.origin;
|
||||
let rel_max=aabb.max()-ray.origin;
|
||||
let dy=rel_max.x*ray.direction.y;
|
||||
let dz=rel_max.x*ray.direction.z;
|
||||
// x is negative, so inequalities are flipped
|
||||
if rel_min.y*ray.direction.x>dy&&dy>rel_max.y*ray.direction.x
|
||||
&&rel_min.z*ray.direction.x>dz&&dz>rel_max.z*ray.direction.x{
|
||||
let t=rel_max.x/ray.direction.x;
|
||||
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
|
||||
}
|
||||
},
|
||||
Ordering::Equal=>(),
|
||||
Ordering::Greater=>{
|
||||
let rel_min=aabb.min()-ray.origin;
|
||||
let rel_max=aabb.max()-ray.origin;
|
||||
let dy=rel_min.x*ray.direction.y;
|
||||
let dz=rel_min.x*ray.direction.z;
|
||||
// x is positive, so inequalities are normal
|
||||
if rel_min.y*ray.direction.x<dy&&dy<rel_max.y*ray.direction.x
|
||||
&&rel_min.z*ray.direction.x<dz&&dz<rel_max.z*ray.direction.x{
|
||||
let t=rel_min.x/ray.direction.x;
|
||||
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
|
||||
}
|
||||
},
|
||||
}
|
||||
match ray.direction.z.cmp(&Planar64::ZERO){
|
||||
Ordering::Less=>{
|
||||
let rel_min=aabb.min()-ray.origin;
|
||||
let rel_max=aabb.max()-ray.origin;
|
||||
let dx=rel_max.z*ray.direction.x;
|
||||
let dy=rel_max.z*ray.direction.y;
|
||||
// z is negative, so inequalities are flipped
|
||||
if rel_min.x*ray.direction.z>dx&&dx>rel_max.x*ray.direction.z
|
||||
&&rel_min.y*ray.direction.z>dy&&dy>rel_max.y*ray.direction.z{
|
||||
let t=rel_max.z/ray.direction.z;
|
||||
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
|
||||
}
|
||||
},
|
||||
Ordering::Equal=>(),
|
||||
Ordering::Greater=>{
|
||||
let rel_min=aabb.min()-ray.origin;
|
||||
let rel_max=aabb.max()-ray.origin;
|
||||
let dx=rel_min.z*ray.direction.x;
|
||||
let dy=rel_min.z*ray.direction.y;
|
||||
// z is positive, so inequalities are normal
|
||||
if rel_min.x*ray.direction.z<dx&&dx<rel_max.x*ray.direction.z
|
||||
&&rel_min.y*ray.direction.z<dy&&dy<rel_max.y*ray.direction.z{
|
||||
let t=rel_min.z/ray.direction.z;
|
||||
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
|
||||
}
|
||||
},
|
||||
}
|
||||
match ray.direction.y.cmp(&Planar64::ZERO){
|
||||
Ordering::Less=>{
|
||||
let rel_min=aabb.min()-ray.origin;
|
||||
let rel_max=aabb.max()-ray.origin;
|
||||
let dz=rel_max.y*ray.direction.z;
|
||||
let dx=rel_max.y*ray.direction.x;
|
||||
// y is negative, so inequalities are flipped
|
||||
if rel_min.z*ray.direction.y>dz&&dz>rel_max.z*ray.direction.y
|
||||
&&rel_min.x*ray.direction.y>dx&&dx>rel_max.x*ray.direction.y{
|
||||
let t=rel_max.y/ray.direction.y;
|
||||
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
|
||||
}
|
||||
},
|
||||
Ordering::Equal=>(),
|
||||
Ordering::Greater=>{
|
||||
let rel_min=aabb.min()-ray.origin;
|
||||
let rel_max=aabb.max()-ray.origin;
|
||||
let dz=rel_min.y*ray.direction.z;
|
||||
let dx=rel_min.y*ray.direction.x;
|
||||
// y is positive, so inequalities are normal
|
||||
if rel_min.z*ray.direction.y<dz&&dz<rel_max.z*ray.direction.y
|
||||
&&rel_min.x*ray.direction.y<dx&&dx<rel_max.x*ray.direction.y{
|
||||
let t=rel_min.y/ray.direction.y;
|
||||
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
|
||||
}
|
||||
},
|
||||
}
|
||||
hit
|
||||
}
|
||||
impl<R,T> Default for RecursiveContent<R,T>{
|
||||
fn default()->Self{
|
||||
|
||||
pub enum RecursiveContent<N,L>{
|
||||
Branch(Vec<N>),
|
||||
Leaf(L),
|
||||
}
|
||||
impl<N,L> RecursiveContent<N,L>{
|
||||
pub fn empty()->Self{
|
||||
Self::Branch(Vec::new())
|
||||
}
|
||||
}
|
||||
pub struct BvhNode<T>{
|
||||
content:RecursiveContent<BvhNode<T>,T>,
|
||||
pub struct BvhNode<L>{
|
||||
content:RecursiveContent<BvhNode<L>,L>,
|
||||
aabb:Aabb,
|
||||
}
|
||||
impl<T> Default for BvhNode<T>{
|
||||
fn default()->Self{
|
||||
impl<L> BvhNode<L>{
|
||||
pub fn empty()->Self{
|
||||
Self{
|
||||
content:Default::default(),
|
||||
content:RecursiveContent::empty(),
|
||||
aabb:Aabb::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct BvhWeightNode<W,T>{
|
||||
content:RecursiveContent<BvhWeightNode<W,T>,T>,
|
||||
weight:W,
|
||||
aabb:Aabb,
|
||||
}
|
||||
|
||||
impl<T> BvhNode<T>{
|
||||
pub fn the_tester<F:FnMut(&T)>(&self,aabb:&Aabb,f:&mut F){
|
||||
pub fn sample_aabb<F:FnMut(&L)>(&self,aabb:&Aabb,f:&mut F){
|
||||
match &self.content{
|
||||
RecursiveContent::Leaf(model)=>f(model),
|
||||
RecursiveContent::Branch(children)=>for child in children{
|
||||
@ -47,51 +135,101 @@ impl<T> BvhNode<T>{
|
||||
//you're probably not going to spend a lot of time outside the map,
|
||||
//so the test is extra work for nothing
|
||||
if aabb.intersects(&child.aabb){
|
||||
child.the_tester(aabb,f);
|
||||
child.sample_aabb(aabb,f);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
|
||||
match self.content{
|
||||
RecursiveContent::Leaf(model)=>f(model),
|
||||
fn populate_nodes<'a,T,F>(
|
||||
&'a self,
|
||||
collector:&mut InstructionCollector<&'a L,Ratio<Planar64,Planar64>>,
|
||||
nodes:&mut BTreeMap<Ratio<Planar64,Planar64>,&'a BvhNode<L>>,
|
||||
ray:&Ray,
|
||||
start_time:Ratio<Planar64,Planar64>,
|
||||
f:&F,
|
||||
)
|
||||
where
|
||||
T:Ord+Copy,
|
||||
Ratio<Planar64,Planar64>:From<T>,
|
||||
F:Fn(&L,&Ray)->Option<T>,
|
||||
{
|
||||
match &self.content{
|
||||
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
|
||||
let ins=TimedInstruction{time:time.into(),instruction:leaf};
|
||||
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
|
||||
collector.collect(Some(ins));
|
||||
}
|
||||
},
|
||||
RecursiveContent::Branch(children)=>for child in children{
|
||||
child.into_visitor(f)
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn weigh_contents<W:Copy+std::iter::Sum<W>,F:Fn(&T)->W>(self,f:&F)->BvhWeightNode<W,T>{
|
||||
match self.content{
|
||||
RecursiveContent::Leaf(model)=>BvhWeightNode{
|
||||
weight:f(&model),
|
||||
content:RecursiveContent::Leaf(model),
|
||||
aabb:self.aabb,
|
||||
},
|
||||
RecursiveContent::Branch(children)=>{
|
||||
let branch:Vec<BvhWeightNode<W,T>>=children.into_iter().map(|child|
|
||||
child.weigh_contents(f)
|
||||
).collect();
|
||||
BvhWeightNode{
|
||||
weight:branch.iter().map(|node|node.weight).sum(),
|
||||
content:RecursiveContent::Branch(branch),
|
||||
aabb:self.aabb,
|
||||
if child.aabb.contains(ray.origin){
|
||||
child.populate_nodes(collector,nodes,ray,start_time,f);
|
||||
}else{
|
||||
// Am I an upcoming superstar?
|
||||
if let Some(t)=intersect_aabb(ray,&child.aabb){
|
||||
if start_time.lt_ratio(t)&&t.lt_ratio(collector.time()){
|
||||
nodes.insert(t,child);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn sample_ray<T,F>(
|
||||
&self,
|
||||
ray:&Ray,
|
||||
start_time:T,
|
||||
time_limit:T,
|
||||
f:F,
|
||||
)->Option<(T,&L)>
|
||||
where
|
||||
T:Ord+Copy,
|
||||
T:From<Ratio<Planar64,Planar64>>,
|
||||
Ratio<Planar64,Planar64>:From<T>,
|
||||
F:Fn(&L,&Ray)->Option<T>,
|
||||
{
|
||||
// source of nondeterminism when Aabb boundaries are coplanar
|
||||
let mut nodes=BTreeMap::new();
|
||||
|
||||
impl <W,T> BvhWeightNode<W,T>{
|
||||
pub const fn weight(&self)->&W{
|
||||
&self.weight
|
||||
let start_time=start_time.into();
|
||||
let time_limit=time_limit.into();
|
||||
let mut collector=InstructionCollector::new(time_limit);
|
||||
// break open all nodes that contain ray.origin and populate nodes with future intersection times
|
||||
self.populate_nodes(&mut collector,&mut nodes,ray,start_time,&f);
|
||||
|
||||
// swim through nodes one at a time
|
||||
while let Some((t,node))=nodes.pop_first(){
|
||||
if collector.time()<t{
|
||||
break;
|
||||
}
|
||||
match &node.content{
|
||||
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
|
||||
let ins=TimedInstruction{time:time.into(),instruction:leaf};
|
||||
// this lower bound can also be omitted
|
||||
// but it causes type inference errors lol
|
||||
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
|
||||
collector.collect(Some(ins));
|
||||
}
|
||||
},
|
||||
// break open the node and predict collisions with the child nodes
|
||||
RecursiveContent::Branch(children)=>for child in children{
|
||||
// Am I an upcoming superstar?
|
||||
if let Some(t)=intersect_aabb(ray,&child.aabb){
|
||||
// we don't need to check the lower bound
|
||||
// because child aabbs are guaranteed to be within the parent bounds.
|
||||
if t<collector.time(){
|
||||
nodes.insert(t,child);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
collector.take().map(|TimedInstruction{time,instruction:leaf}|(time.into(),leaf))
|
||||
}
|
||||
pub const fn aabb(&self)->&Aabb{
|
||||
&self.aabb
|
||||
pub fn into_inner(self)->(RecursiveContent<BvhNode<L>,L>,Aabb){
|
||||
(self.content,self.aabb)
|
||||
}
|
||||
pub fn into_content(self)->RecursiveContent<BvhWeightNode<W,T>,T>{
|
||||
self.content
|
||||
}
|
||||
pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
|
||||
pub fn into_visitor<F:FnMut(L)>(self,f:&mut F){
|
||||
match self.content{
|
||||
RecursiveContent::Leaf(model)=>f(model),
|
||||
RecursiveContent::Branch(children)=>for child in children{
|
||||
@ -130,9 +268,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
|
||||
sort_y.push((i,center.y));
|
||||
sort_z.push((i,center.z));
|
||||
}
|
||||
sort_x.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
|
||||
sort_y.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
|
||||
sort_z.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
|
||||
sort_x.sort_by_key(|&(_,c)|c);
|
||||
sort_y.sort_by_key(|&(_,c)|c);
|
||||
sort_z.sort_by_key(|&(_,c)|c);
|
||||
let h=n/2;
|
||||
let median_x=sort_x[h].1;
|
||||
let median_y=sort_y[h].1;
|
||||
|
@ -171,4 +171,7 @@ impl CollisionAttributes{
|
||||
pub fn contact_default()->Self{
|
||||
Self::Contact(ContactAttributes::default())
|
||||
}
|
||||
pub fn intersect_default()->Self{
|
||||
Self::Intersect(IntersectAttributes::default())
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use std::collections::{HashSet,HashMap};
|
||||
use crate::model::ModelId;
|
||||
use crate::gameplay_style;
|
||||
use crate::updatable::Updatable;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StageElement{
|
||||
@ -128,18 +127,6 @@ impl Stage{
|
||||
self.unordered_checkpoints.contains(&model_id)
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct StageUpdate{
|
||||
//other behaviour models of this stage can have
|
||||
ordered_checkpoints:HashMap<CheckpointId,ModelId>,
|
||||
unordered_checkpoints:HashSet<ModelId>,
|
||||
}
|
||||
impl Updatable<StageUpdate> for Stage{
|
||||
fn update(&mut self,update:StageUpdate){
|
||||
self.ordered_checkpoints.extend(update.ordered_checkpoints);
|
||||
self.unordered_checkpoints.extend(update.unordered_checkpoints);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
|
||||
pub enum Zone{
|
||||
@ -211,34 +198,47 @@ impl Mode{
|
||||
pub fn push_stage(&mut self,stage:Stage){
|
||||
self.stages.push(stage)
|
||||
}
|
||||
pub fn get_stage_mut(&mut self,stage:StageId)->Option<&mut Stage>{
|
||||
self.stages.get_mut(stage.0 as usize)
|
||||
pub fn get_stage_mut(&mut self,StageId(stage_id):StageId)->Option<&mut Stage>{
|
||||
self.stages.get_mut(stage_id as usize)
|
||||
}
|
||||
pub fn get_spawn_model_id(&self,stage:StageId)->Option<ModelId>{
|
||||
self.stages.get(stage.0 as usize).map(|s|s.spawn)
|
||||
pub fn get_spawn_model_id(&self,StageId(stage_id):StageId)->Option<ModelId>{
|
||||
self.stages.get(stage_id as usize).map(|s|s.spawn)
|
||||
}
|
||||
pub fn get_zone(&self,model_id:ModelId)->Option<&Zone>{
|
||||
self.zones.get(&model_id)
|
||||
}
|
||||
pub fn get_stage(&self,stage_id:StageId)->Option<&Stage>{
|
||||
self.stages.get(stage_id.0 as usize)
|
||||
pub fn get_stage(&self,StageId(stage_id):StageId)->Option<&Stage>{
|
||||
self.stages.get(stage_id as usize)
|
||||
}
|
||||
pub fn get_element(&self,model_id:ModelId)->Option<&StageElement>{
|
||||
self.elements.get(&model_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NormalizedMode(Mode);
|
||||
impl NormalizedMode{
|
||||
pub fn new(mode:Mode)->Self{
|
||||
Self(mode)
|
||||
}
|
||||
pub fn into_inner(self)->Mode{
|
||||
let Self(mode)=self;
|
||||
mode
|
||||
}
|
||||
//TODO: put this in the SNF
|
||||
pub fn denormalize_data(&mut self){
|
||||
pub fn denormalize(self)->Mode{
|
||||
let NormalizedMode(mut mode)=self;
|
||||
//expand and index normalized data
|
||||
self.zones.insert(self.start,Zone::Start);
|
||||
for (stage_id,stage) in self.stages.iter().enumerate(){
|
||||
self.elements.insert(stage.spawn,StageElement{
|
||||
mode.zones.insert(mode.start,Zone::Start);
|
||||
for (stage_id,stage) in mode.stages.iter().enumerate(){
|
||||
mode.elements.insert(stage.spawn,StageElement{
|
||||
stage_id:StageId(stage_id as u32),
|
||||
force:false,
|
||||
behaviour:StageElementBehaviour::SpawnAt,
|
||||
jump_limit:None,
|
||||
});
|
||||
for (_,&model) in &stage.ordered_checkpoints{
|
||||
self.elements.insert(model,StageElement{
|
||||
mode.elements.insert(model,StageElement{
|
||||
stage_id:StageId(stage_id as u32),
|
||||
force:false,
|
||||
behaviour:StageElementBehaviour::Checkpoint,
|
||||
@ -246,7 +246,7 @@ impl Mode{
|
||||
});
|
||||
}
|
||||
for &model in &stage.unordered_checkpoints{
|
||||
self.elements.insert(model,StageElement{
|
||||
mode.elements.insert(model,StageElement{
|
||||
stage_id:StageId(stage_id as u32),
|
||||
force:false,
|
||||
behaviour:StageElementBehaviour::Checkpoint,
|
||||
@ -254,53 +254,13 @@ impl Mode{
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//this would be nice as a macro
|
||||
#[derive(Default)]
|
||||
pub struct ModeUpdate{
|
||||
zones:HashMap<ModelId,Zone>,
|
||||
stages:HashMap<StageId,StageUpdate>,
|
||||
//mutually exlusive stage element behaviour
|
||||
elements:HashMap<ModelId,StageElement>,
|
||||
}
|
||||
impl Updatable<ModeUpdate> for Mode{
|
||||
fn update(&mut self,update:ModeUpdate){
|
||||
self.zones.extend(update.zones);
|
||||
for (stage,stage_update) in update.stages{
|
||||
if let Some(stage)=self.stages.get_mut(stage.0 as usize){
|
||||
stage.update(stage_update);
|
||||
}
|
||||
}
|
||||
self.elements.extend(update.elements);
|
||||
}
|
||||
}
|
||||
impl ModeUpdate{
|
||||
pub fn zone(model_id:ModelId,zone:Zone)->Self{
|
||||
let mut mu=Self::default();
|
||||
mu.zones.insert(model_id,zone);
|
||||
mu
|
||||
}
|
||||
pub fn stage(stage_id:StageId,stage_update:StageUpdate)->Self{
|
||||
let mut mu=Self::default();
|
||||
mu.stages.insert(stage_id,stage_update);
|
||||
mu
|
||||
}
|
||||
pub fn element(model_id:ModelId,element:StageElement)->Self{
|
||||
let mut mu=Self::default();
|
||||
mu.elements.insert(model_id,element);
|
||||
mu
|
||||
}
|
||||
pub fn map_stage_element_ids<F:Fn(StageId)->StageId>(&mut self,f:F){
|
||||
for (_,stage_element) in self.elements.iter_mut(){
|
||||
stage_element.stage_id=f(stage_element.stage_id);
|
||||
}
|
||||
mode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Clone)]
|
||||
pub struct Modes{
|
||||
pub modes:Vec<Mode>,
|
||||
modes:Vec<Mode>,
|
||||
}
|
||||
impl Modes{
|
||||
pub const fn new(modes:Vec<Mode>)->Self{
|
||||
@ -314,19 +274,191 @@ impl Modes{
|
||||
pub fn push_mode(&mut self,mode:Mode){
|
||||
self.modes.push(mode)
|
||||
}
|
||||
pub fn get_mode(&self,mode:ModeId)->Option<&Mode>{
|
||||
self.modes.get(mode.0 as usize)
|
||||
pub fn get_mode(&self,ModeId(mode_id):ModeId)->Option<&Mode>{
|
||||
self.modes.get(mode_id as usize)
|
||||
}
|
||||
}
|
||||
pub struct ModesUpdate{
|
||||
modes:HashMap<ModeId,ModeUpdate>,
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NormalizedModes{
|
||||
modes:Vec<NormalizedMode>,
|
||||
}
|
||||
impl Updatable<ModesUpdate> for Modes{
|
||||
fn update(&mut self,update:ModesUpdate){
|
||||
for (mode,mode_update) in update.modes{
|
||||
if let Some(mode)=self.modes.get_mut(mode.0 as usize){
|
||||
mode.update(mode_update);
|
||||
}
|
||||
impl NormalizedModes{
|
||||
pub fn new(modes:Vec<NormalizedMode>)->Self{
|
||||
Self{modes}
|
||||
}
|
||||
pub fn len(&self)->usize{
|
||||
self.modes.len()
|
||||
}
|
||||
pub fn denormalize(self)->Modes{
|
||||
Modes{
|
||||
modes:self.modes.into_iter().map(NormalizedMode::denormalize).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl IntoIterator for NormalizedModes{
|
||||
type Item=<Vec<NormalizedMode> as IntoIterator>::Item;
|
||||
type IntoIter=<Vec<NormalizedMode> as IntoIterator>::IntoIter;
|
||||
fn into_iter(self)->Self::IntoIter{
|
||||
self.modes.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StageUpdate{
|
||||
//other behaviour models of this stage can have
|
||||
ordered_checkpoints:HashMap<CheckpointId,ModelId>,
|
||||
unordered_checkpoints:HashSet<ModelId>,
|
||||
}
|
||||
impl StageUpdate{
|
||||
fn apply_to(self,stage:&mut Stage){
|
||||
stage.ordered_checkpoints.extend(self.ordered_checkpoints);
|
||||
stage.unordered_checkpoints.extend(self.unordered_checkpoints);
|
||||
}
|
||||
}
|
||||
|
||||
//this would be nice as a macro
|
||||
#[derive(Default)]
|
||||
pub struct ModeUpdate{
|
||||
zones:Option<(ModelId,Zone)>,
|
||||
stages:Option<(StageId,StageUpdate)>,
|
||||
//mutually exlusive stage element behaviour
|
||||
elements:Option<(ModelId,StageElement)>,
|
||||
}
|
||||
impl ModeUpdate{
|
||||
fn apply_to(self,mode:&mut Mode){
|
||||
mode.zones.extend(self.zones);
|
||||
if let Some((StageId(stage_id),stage_update))=self.stages{
|
||||
if let Some(stage)=mode.stages.get_mut(stage_id as usize){
|
||||
stage_update.apply_to(stage);
|
||||
}
|
||||
}
|
||||
mode.elements.extend(self.elements);
|
||||
}
|
||||
}
|
||||
impl ModeUpdate{
|
||||
pub fn zone(model_id:ModelId,zone:Zone)->Self{
|
||||
Self{
|
||||
zones:Some((model_id,zone)),
|
||||
stages:None,
|
||||
elements:None,
|
||||
}
|
||||
}
|
||||
pub fn stage(stage_id:StageId,stage_update:StageUpdate)->Self{
|
||||
Self{
|
||||
zones:None,
|
||||
stages:Some((stage_id,stage_update)),
|
||||
elements:None,
|
||||
}
|
||||
}
|
||||
pub fn element(model_id:ModelId,element:StageElement)->Self{
|
||||
Self{
|
||||
zones:None,
|
||||
stages:None,
|
||||
elements:Some((model_id,element)),
|
||||
}
|
||||
}
|
||||
pub fn map_stage_element_ids<F:Fn(StageId)->StageId>(&mut self,f:F){
|
||||
for (_,stage_element) in self.elements.iter_mut(){
|
||||
stage_element.stage_id=f(stage_element.stage_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ModeBuilder{
|
||||
mode:Mode,
|
||||
stage_id_map:HashMap<StageId,StageId>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct ModesBuilder{
|
||||
modes:HashMap<ModeId,Mode>,
|
||||
stages:HashMap<ModeId,HashMap<StageId,Stage>>,
|
||||
mode_updates:Vec<(ModeId,ModeUpdate)>,
|
||||
stage_updates:Vec<(ModeId,StageId,StageUpdate)>,
|
||||
}
|
||||
impl ModesBuilder{
|
||||
pub fn build_normalized(mut self)->NormalizedModes{
|
||||
//collect modes and stages into contiguous arrays
|
||||
let mut unique_modes:Vec<(ModeId,Mode)>
|
||||
=self.modes.into_iter().collect();
|
||||
unique_modes.sort_by_key(|&(mode_id,_)|mode_id);
|
||||
let (mut modes,mode_id_map):(Vec<ModeBuilder>,HashMap<ModeId,ModeId>)
|
||||
=unique_modes.into_iter().enumerate()
|
||||
.map(|(final_mode_id,(builder_mode_id,mut mode))|{
|
||||
(
|
||||
ModeBuilder{
|
||||
stage_id_map:self.stages.remove(&builder_mode_id).map_or_else(||HashMap::new(),|stages|{
|
||||
let mut unique_stages:Vec<(StageId,Stage)>
|
||||
=stages.into_iter().collect();
|
||||
unique_stages.sort_by_key(|&(StageId(stage_id),_)|stage_id);
|
||||
unique_stages.into_iter().enumerate()
|
||||
.map(|(final_stage_id,(builder_stage_id,stage))|{
|
||||
mode.push_stage(stage);
|
||||
(builder_stage_id,StageId::new(final_stage_id as u32))
|
||||
}).collect()
|
||||
}),
|
||||
mode,
|
||||
},
|
||||
(
|
||||
builder_mode_id,
|
||||
ModeId::new(final_mode_id as u32)
|
||||
)
|
||||
)
|
||||
}).unzip();
|
||||
//TODO: failure messages or errors or something
|
||||
//push stage updates
|
||||
for (builder_mode_id,builder_stage_id,stage_update) in self.stage_updates{
|
||||
if let Some(final_mode_id)=mode_id_map.get(&builder_mode_id){
|
||||
if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
|
||||
if let Some(&final_stage_id)=mode.stage_id_map.get(&builder_stage_id){
|
||||
if let Some(stage)=mode.mode.get_stage_mut(final_stage_id){
|
||||
stage_update.apply_to(stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//push mode updates
|
||||
for (builder_mode_id,mut mode_update) in self.mode_updates{
|
||||
if let Some(final_mode_id)=mode_id_map.get(&builder_mode_id){
|
||||
if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
|
||||
//map stage id on stage elements
|
||||
mode_update.map_stage_element_ids(|stage_id|
|
||||
//walk down one stage id at a time until a stage is found
|
||||
//TODO use better logic like BTreeMap::upper_bound instead of walking
|
||||
// final_stage_id_from_builder_stage_id.upper_bound(Bound::Included(&stage_id))
|
||||
// .value().copied().unwrap_or(StageId::FIRST)
|
||||
(0..=stage_id.get()).rev().find_map(|builder_stage_id|
|
||||
//map the stage element to that stage
|
||||
mode.stage_id_map.get(&StageId::new(builder_stage_id)).copied()
|
||||
).unwrap_or(StageId::FIRST)
|
||||
);
|
||||
mode_update.apply_to(&mut mode.mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect())
|
||||
}
|
||||
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode)->Result<(),ExistingEntryError>{
|
||||
error_if_exists(self.modes.insert(mode_id,mode))
|
||||
}
|
||||
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage)->Result<(),ExistingEntryError>{
|
||||
error_if_exists(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage))
|
||||
}
|
||||
pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){
|
||||
self.mode_updates.push((mode_id,mode_update));
|
||||
}
|
||||
// fn push_stage_update(&mut self,mode_id:ModeId,stage_id:StageId,stage_update:StageUpdate){
|
||||
// self.stage_updates.push((mode_id,stage_id,stage_update));
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExistingEntryError;
|
||||
fn error_if_exists<T>(value:Option<T>)->Result<(),ExistingEntryError>{
|
||||
match value{
|
||||
Some(_)=>Err(ExistingEntryError),
|
||||
None=>Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -63,22 +63,22 @@ impl JumpImpulse{
|
||||
velocity:Planar64Vec3,
|
||||
jump_dir:Planar64Vec3,
|
||||
gravity:&Planar64Vec3,
|
||||
mass:Planar64,
|
||||
_mass:Planar64,
|
||||
)->Planar64Vec3{
|
||||
match self{
|
||||
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().fix_1()),
|
||||
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_1()),
|
||||
&JumpImpulse::Height(height)=>{
|
||||
//height==-v.y*v.y/(2*g.y);
|
||||
//use energy to determine max height
|
||||
let gg=gravity.length_squared();
|
||||
let g=gg.sqrt().fix_1();
|
||||
let g=gg.sqrt();
|
||||
let v_g=gravity.dot(velocity);
|
||||
//do it backwards
|
||||
let radicand=v_g*v_g+(g*height*2).fix_4();
|
||||
velocity-(*gravity*(radicand.sqrt().fix_2()+v_g)/gg).divide().fix_1()
|
||||
let radicand=v_g*v_g+(g*height*2).widen_4();
|
||||
velocity-(*gravity*(radicand.sqrt().wrap_2()+v_g)/gg).divide().clamp_1()
|
||||
},
|
||||
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().fix_1(),
|
||||
&JumpImpulse::Energy(energy)=>{
|
||||
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_1(),
|
||||
&JumpImpulse::Energy(_energy)=>{
|
||||
//calculate energy
|
||||
//let e=gravity.dot(velocity);
|
||||
//add
|
||||
@ -91,10 +91,10 @@ impl JumpImpulse{
|
||||
pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
|
||||
//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
|
||||
match self{
|
||||
&JumpImpulse::Time(time)=>(gravity.length().fix_1()*time/2).divide().fix_1(),
|
||||
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().fix_1(),
|
||||
&JumpImpulse::Time(time)=>(gravity.length().wrap_1()*time/2).divide().clamp_1(),
|
||||
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_1(),
|
||||
&JumpImpulse::Linear(deltav)=>deltav,
|
||||
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().fix_1(),
|
||||
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_1(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,10 +126,10 @@ impl JumpSettings{
|
||||
None=>rel_velocity,
|
||||
};
|
||||
let j=boost_vel.dot(jump_dir);
|
||||
let js=jump_speed.fix_2();
|
||||
let js=jump_speed.widen_2();
|
||||
if j<js{
|
||||
//weak booster: just do a regular jump
|
||||
boost_vel+jump_dir.with_length(js-j).divide().fix_1()
|
||||
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
|
||||
}else{
|
||||
//activate booster normally, jump does nothing
|
||||
boost_vel
|
||||
@ -142,13 +142,13 @@ impl JumpSettings{
|
||||
None=>rel_velocity,
|
||||
};
|
||||
let j=boost_vel.dot(jump_dir);
|
||||
let js=jump_speed.fix_2();
|
||||
let js=jump_speed.widen_2();
|
||||
if j<js{
|
||||
//speed in direction of jump cannot be lower than amount
|
||||
boost_vel+jump_dir.with_length(js-j).divide().fix_1()
|
||||
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
|
||||
}else{
|
||||
//boost and jump add together
|
||||
boost_vel+jump_dir.with_length(js).divide().fix_1()
|
||||
boost_vel+jump_dir.with_length(js).divide().wrap_1()
|
||||
}
|
||||
}
|
||||
(false,JumpCalculation::Max)=>{
|
||||
@ -159,10 +159,10 @@ impl JumpSettings{
|
||||
None=>rel_velocity,
|
||||
};
|
||||
let boost_dot=boost_vel.dot(jump_dir);
|
||||
let js=jump_speed.fix_2();
|
||||
let js=jump_speed.widen_2();
|
||||
if boost_dot<js{
|
||||
//weak boost is extended to jump speed
|
||||
boost_vel+jump_dir.with_length(js-boost_dot).divide().fix_1()
|
||||
boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_1()
|
||||
}else{
|
||||
//activate booster normally, jump does nothing
|
||||
boost_vel
|
||||
@ -174,7 +174,7 @@ impl JumpSettings{
|
||||
Some(booster)=>booster.boost(rel_velocity),
|
||||
None=>rel_velocity,
|
||||
};
|
||||
boost_vel+jump_dir.with_length(jump_speed).divide().fix_1()
|
||||
boost_vel+jump_dir.with_length(jump_speed).divide().wrap_1()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -267,9 +267,9 @@ pub struct StrafeSettings{
|
||||
impl StrafeSettings{
|
||||
pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
|
||||
let d=velocity.dot(control_dir);
|
||||
let mv=self.mv.fix_2();
|
||||
let mv=self.mv.widen_2();
|
||||
match d<mv{
|
||||
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.fix_2().min(mv-d))).fix_1()),
|
||||
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_2().min(mv-d))).wrap_1()),
|
||||
false=>None,
|
||||
}
|
||||
}
|
||||
@ -290,7 +290,7 @@ pub struct PropulsionSettings{
|
||||
}
|
||||
impl PropulsionSettings{
|
||||
pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
|
||||
(control_dir*self.magnitude).fix_1()
|
||||
(control_dir*self.magnitude).clamp_1()
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,13 +310,13 @@ pub struct WalkSettings{
|
||||
impl WalkSettings{
|
||||
pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
|
||||
//TODO: fallible walk accel
|
||||
let diff_len=target_diff.length().fix_1();
|
||||
let diff_len=target_diff.length().wrap_1();
|
||||
let friction=if diff_len<self.accelerate.topspeed{
|
||||
self.static_friction
|
||||
}else{
|
||||
self.kinetic_friction
|
||||
};
|
||||
self.accelerate.accel.min((-gravity.y*friction).fix_1())
|
||||
self.accelerate.accel.min((-gravity.y*friction).clamp_1())
|
||||
}
|
||||
pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
|
||||
if control_dir==crate::integer::vec3::ZERO{
|
||||
@ -332,7 +332,7 @@ impl WalkSettings{
|
||||
if cr==crate::integer::vec3::ZERO_2{
|
||||
crate::integer::vec3::ZERO
|
||||
}else{
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().fix_1()
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
|
||||
}
|
||||
}else{
|
||||
crate::integer::vec3::ZERO
|
||||
@ -341,7 +341,7 @@ impl WalkSettings{
|
||||
pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
|
||||
//normal is not guaranteed to be unit length
|
||||
let ny=normal.dot(up);
|
||||
let h=normal.length().fix_1();
|
||||
let h=normal.length().wrap_1();
|
||||
//remember this is a normal vector
|
||||
ny.is_positive()&&h*self.surf_dot<ny
|
||||
}
|
||||
@ -355,7 +355,7 @@ pub struct LadderSettings{
|
||||
pub dot:Planar64,
|
||||
}
|
||||
impl LadderSettings{
|
||||
pub const fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
|
||||
pub const fn accel(&self,_target_diff:Planar64Vec3,_gravity:Planar64Vec3)->Planar64{
|
||||
//TODO: fallible ladder accel
|
||||
self.accelerate.accel
|
||||
}
|
||||
@ -368,13 +368,13 @@ impl LadderSettings{
|
||||
let nnmm=nn*mm;
|
||||
let d=normal.dot(control_dir);
|
||||
let mut dd=d*d;
|
||||
if (self.dot*self.dot*nnmm).fix_4()<dd{
|
||||
if (self.dot*self.dot*nnmm).clamp_4()<dd{
|
||||
if d.is_negative(){
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.fix_1(),Planar64::ZERO]);
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_1(),Planar64::ZERO]);
|
||||
}else{
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.fix_1(),Planar64::ZERO]);
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_1(),Planar64::ZERO]);
|
||||
}
|
||||
dd=(normal.y*normal.y).fix_4();
|
||||
dd=(normal.y*normal.y).widen_4();
|
||||
}
|
||||
//n=d if you are standing on top of a ladder and press E.
|
||||
//two fixes:
|
||||
@ -385,7 +385,7 @@ impl LadderSettings{
|
||||
if cr==crate::integer::vec3::ZERO_2{
|
||||
crate::integer::vec3::ZERO
|
||||
}else{
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().fix_1()
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
|
||||
}
|
||||
}else{
|
||||
crate::integer::vec3::ZERO
|
||||
@ -417,7 +417,7 @@ impl Hitbox{
|
||||
}
|
||||
pub fn source()->Self{
|
||||
Self{
|
||||
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).fix_1(),
|
||||
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_1().unwrap(),
|
||||
mesh:HitboxMesh::Box,
|
||||
}
|
||||
}
|
||||
@ -529,20 +529,20 @@ impl StyleModifiers{
|
||||
|
||||
pub fn source_bhop()->Self{
|
||||
Self{
|
||||
controls_mask:Controls::all()-Controls::MoveUp-Controls::MoveDown,
|
||||
controls_mask:Controls::all(),
|
||||
controls_mask_state:Controls::all(),
|
||||
strafe:Some(StrafeSettings{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:Some(Planar64::raw(150<<28)*100),
|
||||
mv:(Planar64::raw(30)*VALVE_SCALE).fix_1(),
|
||||
air_accel_limit:Some(Planar64::raw((150<<28)*100)),
|
||||
mv:Planar64::raw(30<<28),
|
||||
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
|
||||
calculation:JumpCalculation::JumpThenBoost,
|
||||
limit_minimum:true,
|
||||
}),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).fix_1(),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
|
||||
mass:int(1),
|
||||
rocket:None,
|
||||
walk:Some(WalkSettings{
|
||||
@ -565,25 +565,25 @@ impl StyleModifiers{
|
||||
magnitude:int(12),//?
|
||||
}),
|
||||
hitbox:Hitbox::source(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).fix_1(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
|
||||
}
|
||||
}
|
||||
pub fn source_surf()->Self{
|
||||
Self{
|
||||
controls_mask:Controls::all()-Controls::MoveUp-Controls::MoveDown,
|
||||
controls_mask:Controls::all(),
|
||||
controls_mask_state:Controls::all(),
|
||||
strafe:Some(StrafeSettings{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:Some((int(150)*66*VALVE_SCALE).fix_1()),
|
||||
mv:(int(30)*VALVE_SCALE).fix_1(),
|
||||
air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_1().unwrap()),
|
||||
mv:Planar64::raw(30<<28),
|
||||
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
|
||||
calculation:JumpCalculation::JumpThenBoost,
|
||||
limit_minimum:true,
|
||||
}),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).fix_1(),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
|
||||
mass:int(1),
|
||||
rocket:None,
|
||||
walk:Some(WalkSettings{
|
||||
@ -606,7 +606,7 @@ impl StyleModifiers{
|
||||
magnitude:int(12),//?
|
||||
}),
|
||||
hitbox:Hitbox::source(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).fix_1(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
use crate::integer::Time;
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct TimedInstruction<I,T>{
|
||||
pub time:Time<T>,
|
||||
pub 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>{
|
||||
pub fn set_time<T2>(self,new_time:T2)->TimedInstruction<I,T2>{
|
||||
TimedInstruction{
|
||||
time:new_time,
|
||||
instruction:self.instruction,
|
||||
@ -17,21 +15,21 @@ impl<I,T> TimedInstruction<I,T>{
|
||||
|
||||
/// Ensure all emitted instructions are processed before consuming external instructions
|
||||
pub trait InstructionEmitter<I>{
|
||||
type TimeInner;
|
||||
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
|
||||
type Time;
|
||||
fn next_instruction(&self,time_limit:Self::Time)->Option<TimedInstruction<I,Self::Time>>;
|
||||
}
|
||||
/// Apply an atomic state update
|
||||
pub trait InstructionConsumer<I>{
|
||||
type TimeInner;
|
||||
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
|
||||
type Time;
|
||||
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::Time>);
|
||||
}
|
||||
/// 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>
|
||||
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>
|
||||
where
|
||||
Time<T>:Copy,
|
||||
T:Copy,
|
||||
{
|
||||
#[inline]
|
||||
fn process_exhaustive(&mut self,time_limit:Time<T>){
|
||||
fn process_exhaustive(&mut self,time_limit:T){
|
||||
while let Some(instruction)=self.next_instruction(time_limit){
|
||||
self.process_instruction(instruction);
|
||||
}
|
||||
@ -39,39 +37,24 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+Instruction
|
||||
}
|
||||
impl<I,T,X> InstructionFeedback<I,T> for X
|
||||
where
|
||||
Time<T>:Copy,
|
||||
X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
|
||||
T:Copy,
|
||||
X:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
|
||||
{}
|
||||
|
||||
//PROPER PRIVATE FIELDS!!!
|
||||
pub struct InstructionCollector<I,T>{
|
||||
time:Time<T>,
|
||||
time:T,
|
||||
instruction:Option<I>,
|
||||
}
|
||||
impl<I,T> InstructionCollector<I,T>
|
||||
where Time<T>:Copy+PartialOrd,
|
||||
{
|
||||
impl<I,T> InstructionCollector<I,T>{
|
||||
#[inline]
|
||||
pub const fn new(time:Time<T>)->Self{
|
||||
pub const fn new(time:T)->Self{
|
||||
Self{
|
||||
time,
|
||||
instruction:None
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub const fn time(&self)->Time<T>{
|
||||
self.time
|
||||
}
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn take(self)->Option<TimedInstruction<I,T>>{
|
||||
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
|
||||
self.instruction.map(|instruction|TimedInstruction{
|
||||
@ -80,3 +63,20 @@ impl<I,T> InstructionCollector<I,T>
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<I,T:Copy> InstructionCollector<I,T>{
|
||||
#[inline]
|
||||
pub const fn time(&self)->T{
|
||||
self.time
|
||||
}
|
||||
}
|
||||
impl<I,T:PartialOrd> InstructionCollector<I,T>{
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
pub use fixed_wide::fixed::{Fixed,Fix};
|
||||
pub use fixed_wide::fixed::*;
|
||||
pub use ratio_ops::ratio::{Ratio,Divide};
|
||||
|
||||
//integer units
|
||||
|
||||
/// specific example of a "default" time type
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type AbsoluteTime=Time<TimeInner>;
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
|
||||
pub struct Time<T>(i64,core::marker::PhantomData<T>);
|
||||
impl<T> Time<T>{
|
||||
pub const MIN:Self=Self::raw(i64::MIN);
|
||||
@ -60,18 +60,24 @@ impl<T> Time<T>{
|
||||
impl<T> From<Planar64> for Time<T>{
|
||||
#[inline]
|
||||
fn from(value:Planar64)->Self{
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_1().to_raw())
|
||||
}
|
||||
}
|
||||
impl<T> From<Time<T>> for Ratio<Planar64,Planar64>{
|
||||
#[inline]
|
||||
fn from(value:Time<T>)->Self{
|
||||
value.to_ratio()
|
||||
}
|
||||
}
|
||||
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>,
|
||||
T1:Fix<Planar64>,
|
||||
T1:Clamp<Planar64>,
|
||||
{
|
||||
#[inline]
|
||||
fn from(value:Ratio<Num,Den>)->Self{
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).divide().clamp().to_raw())
|
||||
}
|
||||
}
|
||||
impl<T> std::fmt::Display for Time<T>{
|
||||
@ -395,6 +401,10 @@ impl Angle32{
|
||||
pub const NEG_FRAC_PI_2:Self=Self(-1<<30);
|
||||
pub const PI:Self=Self(-1<<31);
|
||||
#[inline]
|
||||
pub const fn raw(num:i32)->Self{
|
||||
Self(num)
|
||||
}
|
||||
#[inline]
|
||||
pub const fn wrap_from_i64(theta:i64)->Self{
|
||||
//take lower bits
|
||||
//note: this was checked on compiler explorer and compiles to 1 instruction!
|
||||
@ -509,8 +519,8 @@ fn angle_sin_cos(){
|
||||
println!("cordic s={} c={}",(s/h).divide(),(c/h).divide());
|
||||
let (fs,fc)=f.sin_cos();
|
||||
println!("float s={} c={}",fs,fc);
|
||||
assert!(close_enough((c/h).divide().fix_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
|
||||
assert!(close_enough((s/h).divide().fix_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
|
||||
assert!(close_enough((c/h).divide().wrap_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
|
||||
assert!(close_enough((s/h).divide().wrap_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
|
||||
}
|
||||
test_angle(1.0);
|
||||
test_angle(std::f64::consts::PI/4.0);
|
||||
@ -552,6 +562,10 @@ pub mod vec3{
|
||||
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
|
||||
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
|
||||
pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
|
||||
pub const ZERO_3:linear_ops::types::Vector3<Fixed::<3,96>>=linear_ops::types::Vector3::new([Fixed::<3,96>::ZERO;3]);
|
||||
pub const ZERO_4:linear_ops::types::Vector3<Fixed::<4,128>>=linear_ops::types::Vector3::new([Fixed::<4,128>::ZERO;3]);
|
||||
pub const ZERO_5:linear_ops::types::Vector3<Fixed::<5,160>>=linear_ops::types::Vector3::new([Fixed::<5,160>::ZERO;3]);
|
||||
pub const ZERO_6:linear_ops::types::Vector3<Fixed::<6,192>>=linear_ops::types::Vector3::new([Fixed::<6,192>::ZERO;3]);
|
||||
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
|
||||
pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
|
||||
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
|
||||
@ -619,8 +633,8 @@ pub mod mat3{
|
||||
let (yc,ys)=y.cos_sin();
|
||||
Planar64Mat3::from_cols([
|
||||
Planar64Vec3::new([xc,Planar64::ZERO,-xs]),
|
||||
Planar64Vec3::new([(xs*ys).fix_1(),yc,(xc*ys).fix_1()]),
|
||||
Planar64Vec3::new([(xs*yc).fix_1(),-ys,(xc*yc).fix_1()]),
|
||||
Planar64Vec3::new([(xs*ys).wrap_1(),yc,(xc*ys).wrap_1()]),
|
||||
Planar64Vec3::new([(xs*yc).wrap_1(),-ys,(xc*yc).wrap_1()]),
|
||||
])
|
||||
}
|
||||
#[inline]
|
||||
@ -648,13 +662,21 @@ pub struct Planar64Affine3{
|
||||
pub translation:Planar64Vec3,
|
||||
}
|
||||
impl Planar64Affine3{
|
||||
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO);
|
||||
#[inline]
|
||||
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
|
||||
Self{matrix3,translation}
|
||||
}
|
||||
#[inline]
|
||||
pub const fn from_translation(translation:Planar64Vec3)->Self{
|
||||
Self{
|
||||
matrix3:mat3::identity(),
|
||||
translation,
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
|
||||
self.translation.fix_2()+self.matrix3*point
|
||||
self.translation.widen_2()+self.matrix3*point
|
||||
}
|
||||
}
|
||||
impl Into<glam::Mat4> for Planar64Affine3{
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod bvh;
|
||||
pub mod map;
|
||||
pub mod ray;
|
||||
pub mod run;
|
||||
pub mod aabb;
|
||||
pub mod model;
|
||||
@ -8,7 +9,6 @@ pub mod timer;
|
||||
pub mod integer;
|
||||
pub mod physics;
|
||||
pub mod session;
|
||||
pub mod updatable;
|
||||
pub mod instruction;
|
||||
pub mod gameplay_attributes;
|
||||
pub mod gameplay_modes;
|
||||
|
@ -4,7 +4,7 @@ use crate::gameplay_attributes;
|
||||
//this is a temporary struct to try to get the code running again
|
||||
//TODO: use snf::map::Region to update the data in physics and graphics instead of this
|
||||
pub struct CompleteMap{
|
||||
pub modes:gameplay_modes::Modes,
|
||||
pub modes:gameplay_modes::NormalizedModes,
|
||||
pub attributes:Vec<gameplay_attributes::CollisionAttributes>,
|
||||
pub meshes:Vec<model::Mesh>,
|
||||
pub models:Vec<model::Model>,
|
||||
|
@ -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,7 +1,7 @@
|
||||
use crate::mouse::MouseState;
|
||||
use crate::gameplay_modes::{ModeId,StageId};
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type Time=crate::integer::Time<TimeInner>;
|
||||
|
||||
|
20
lib/common/src/ray.rs
Normal file
20
lib/common/src/ray.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use ratio_ops::ratio::Ratio;
|
||||
use crate::integer::{self,Planar64,Planar64Vec3};
|
||||
|
||||
pub struct Ray{
|
||||
pub origin:Planar64Vec3,
|
||||
pub direction:Planar64Vec3,
|
||||
}
|
||||
impl Ray{
|
||||
pub 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::Clamp<Planar64>,
|
||||
{
|
||||
self.origin+self.direction.map(|elem|(t*elem).divide().clamp())
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
|
||||
|
||||
use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime};
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type Time=crate::integer::Time<TimeInner>;
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type Time=crate::integer::Time<TimeInner>;
|
||||
|
@ -23,7 +23,7 @@ impl PauseState for Unpaused{
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
enum Inner{}
|
||||
pub enum Inner{}
|
||||
type InnerTime=Time<Inner>;
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
|
@ -1,56 +0,0 @@
|
||||
pub trait Updatable<Updater>{
|
||||
fn update(&mut self,update:Updater);
|
||||
}
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
|
||||
struct InnerId(u32);
|
||||
#[derive(Clone)]
|
||||
struct Inner{
|
||||
id:InnerId,
|
||||
enabled:bool,
|
||||
}
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
|
||||
struct OuterId(u32);
|
||||
struct Outer{
|
||||
id:OuterId,
|
||||
inners:std::collections::HashMap<InnerId,Inner>,
|
||||
}
|
||||
|
||||
enum Update<I,U>{
|
||||
Insert(I),
|
||||
Update(U),
|
||||
Remove
|
||||
}
|
||||
|
||||
struct InnerUpdate{
|
||||
//#[updatable(Update)]
|
||||
enabled:Option<bool>,
|
||||
}
|
||||
struct OuterUpdate{
|
||||
//#[updatable(Insert,Update,Remove)]
|
||||
inners:std::collections::HashMap<InnerId,Update<Inner,InnerUpdate>>,
|
||||
//#[updatable(Update)]
|
||||
//inners:std::collections::HashMap<InnerId,InnerUpdate>,
|
||||
}
|
||||
impl Updatable<InnerUpdate> for Inner{
|
||||
fn update(&mut self,update:InnerUpdate){
|
||||
if let Some(enabled)=update.enabled{
|
||||
self.enabled=enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Updatable<OuterUpdate> for Outer{
|
||||
fn update(&mut self,update:OuterUpdate){
|
||||
for (id,up) in update.inners{
|
||||
match up{
|
||||
Update::Insert(new_inner)=>self.inners.insert(id,new_inner),
|
||||
Update::Update(inner_update)=>self.inners.get_mut(&id).map(|inner|{
|
||||
let old=inner.clone();
|
||||
inner.update(inner_update);
|
||||
old
|
||||
}),
|
||||
Update::Remove=>self.inners.remove(&id),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
//*/
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "strafesnet_deferred_loader"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Acquire IDs for objects before loading them in bulk."
|
||||
@ -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<'a,L:Loader<Resource=Texture,Index<'a>=H>+'a>(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<'a,L:Loader<Resource=Mesh,Index<'a>=H>+'a>(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<'a> where Self:'a;
|
||||
type Resource;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->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,7 +1,7 @@
|
||||
[package]
|
||||
name = "fixed_wide"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Fixed point numbers with optional widening Mul operator."
|
||||
@ -14,7 +14,7 @@ wide-mul=[]
|
||||
zeroes=["dep:arrayvec"]
|
||||
|
||||
[dependencies]
|
||||
bnum = "0.12.0"
|
||||
bnum = "0.13.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 }
|
||||
|
@ -33,6 +33,14 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
|
||||
self.bits
|
||||
}
|
||||
#[inline]
|
||||
pub const fn as_bits(&self)->&BInt<N>{
|
||||
&self.bits
|
||||
}
|
||||
#[inline]
|
||||
pub const fn as_bits_mut(&mut self)->&mut BInt<N>{
|
||||
&mut self.bits
|
||||
}
|
||||
#[inline]
|
||||
pub const fn raw_digit(value:i64)->Self{
|
||||
let mut digits=[0u64;N];
|
||||
digits[0]=value.abs() as u64;
|
||||
@ -56,6 +64,10 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
|
||||
pub const fn abs(self)->Self{
|
||||
Self::from_bits(self.bits.abs())
|
||||
}
|
||||
#[inline]
|
||||
pub const fn midpoint(self,other:Self)->Self{
|
||||
Self::from_bits(self.bits.midpoint(other.bits))
|
||||
}
|
||||
}
|
||||
impl<const F:usize> Fixed<1,F>{
|
||||
/// My old code called this function everywhere so let's provide it
|
||||
@ -651,74 +663,94 @@ macro_repeated!(
|
||||
1,2,3,4,5,6,7,8
|
||||
);
|
||||
|
||||
pub trait Fix<Out>{
|
||||
fn fix(self)->Out;
|
||||
#[derive(Debug,Eq,PartialEq)]
|
||||
pub enum NarrowError{
|
||||
Overflow,
|
||||
Underflow,
|
||||
}
|
||||
|
||||
macro_rules! impl_fix_rhs_lt_lhs_not_const_generic{
|
||||
pub trait Wrap<Output>{
|
||||
fn wrap(self)->Output;
|
||||
}
|
||||
pub trait Clamp<Output>{
|
||||
fn clamp(self)->Output;
|
||||
}
|
||||
impl<const N:usize,const F:usize> Clamp<Fixed<N,F>> for Result<Fixed<N,F>,NarrowError>{
|
||||
fn clamp(self)->Fixed<N,F>{
|
||||
match self{
|
||||
Ok(fixed)=>fixed,
|
||||
Err(NarrowError::Overflow)=>Fixed::MAX,
|
||||
Err(NarrowError::Underflow)=>Fixed::MIN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_narrow_not_const_generic{
|
||||
(
|
||||
(),
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
impl Fixed<$lhs,{$lhs*32}>
|
||||
{
|
||||
paste::item!{
|
||||
paste::item!{
|
||||
impl Fixed<$lhs,{$lhs*32}>
|
||||
{
|
||||
#[inline]
|
||||
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
|
||||
pub fn [<wrap_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
|
||||
Fixed::from_bits(bnum::cast::As::as_::<BInt::<$rhs>>(self.bits.shr(($lhs-$rhs)*32)))
|
||||
}
|
||||
#[inline]
|
||||
pub fn [<narrow_ $rhs>](self)->Result<Fixed<$rhs,{$rhs*32}>,NarrowError>{
|
||||
if Fixed::<$rhs,{$rhs*32}>::MAX.[<widen_ $lhs>]().bits<self.bits{
|
||||
return Err(NarrowError::Overflow);
|
||||
}
|
||||
if self.bits<Fixed::<$rhs,{$rhs*32}>::MIN.[<widen_ $lhs>]().bits{
|
||||
return Err(NarrowError::Underflow);
|
||||
}
|
||||
Ok(self.[<wrap_ $rhs>]())
|
||||
}
|
||||
#[inline]
|
||||
pub fn [<clamp_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
|
||||
self.[<narrow_ $rhs>]().clamp()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
|
||||
paste::item!{
|
||||
self.[<fix_ $rhs>]()
|
||||
impl Wrap<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
#[inline]
|
||||
fn wrap(self)->Fixed<$rhs,{$rhs*32}>{
|
||||
self.[<wrap_ $rhs>]()
|
||||
}
|
||||
}
|
||||
impl TryInto<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
type Error=NarrowError;
|
||||
#[inline]
|
||||
fn try_into(self)->Result<Fixed<$rhs,{$rhs*32}>,Self::Error>{
|
||||
self.[<narrow_ $rhs>]()
|
||||
}
|
||||
}
|
||||
impl Clamp<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
#[inline]
|
||||
fn clamp(self)->Fixed<$rhs,{$rhs*32}>{
|
||||
self.[<clamp_ $rhs>]()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! impl_fix_lhs_lt_rhs_not_const_generic{
|
||||
macro_rules! impl_widen_not_const_generic{
|
||||
(
|
||||
(),
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
impl Fixed<$lhs,{$lhs*32}>
|
||||
{
|
||||
paste::item!{
|
||||
paste::item!{
|
||||
impl Fixed<$lhs,{$lhs*32}>
|
||||
{
|
||||
#[inline]
|
||||
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
|
||||
pub fn [<widen_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
|
||||
Fixed::from_bits(bnum::cast::As::as_::<BInt::<$rhs>>(self.bits).shl(($rhs-$lhs)*32))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
|
||||
paste::item!{
|
||||
self.[<fix_ $rhs>]()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! impl_fix_lhs_eq_rhs_not_const_generic{
|
||||
(
|
||||
(),
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
impl Fixed<$lhs,{$lhs*32}>
|
||||
{
|
||||
paste::item!{
|
||||
impl Into<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
#[inline]
|
||||
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
|
||||
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
|
||||
paste::item!{
|
||||
self.[<fix_ $rhs>]()
|
||||
fn into(self)->Fixed<$rhs,{$rhs*32}>{
|
||||
self.[<widen_ $rhs>]()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -728,7 +760,7 @@ macro_rules! impl_fix_lhs_eq_rhs_not_const_generic{
|
||||
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
|
||||
|
||||
macro_repeated!(
|
||||
impl_fix_rhs_lt_lhs_not_const_generic,(),
|
||||
impl_narrow_not_const_generic,(),
|
||||
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
|
||||
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
|
||||
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
|
||||
@ -746,7 +778,7 @@ macro_repeated!(
|
||||
(16,15)
|
||||
);
|
||||
macro_repeated!(
|
||||
impl_fix_lhs_lt_rhs_not_const_generic,(),
|
||||
impl_widen_not_const_generic,(),
|
||||
(1,2),
|
||||
(1,3),(2,3),
|
||||
(1,4),(2,4),(3,4),
|
||||
@ -761,11 +793,8 @@ macro_repeated!(
|
||||
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
|
||||
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
|
||||
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
|
||||
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16)
|
||||
);
|
||||
macro_repeated!(
|
||||
impl_fix_lhs_eq_rhs_not_const_generic,(),
|
||||
(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10),(11,11),(12,12),(13,13),(14,14),(15,15),(16,16)
|
||||
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
|
||||
(1,17)
|
||||
);
|
||||
|
||||
macro_rules! impl_not_const_generic{
|
||||
@ -785,16 +814,13 @@ macro_rules! impl_not_const_generic{
|
||||
let mut result=Self::ZERO;
|
||||
|
||||
//resize self to match the wide mul output
|
||||
let wide_self=self.[<fix_ $_2n>]();
|
||||
let wide_self=self.[<widen_ $_2n>]();
|
||||
//descend down the bits and check if flipping each bit would push the square over the input value
|
||||
for shift in (0..=max_shift).rev(){
|
||||
let new_result={
|
||||
let mut bits=result.to_bits().to_bits();
|
||||
bits.set_bit(shift,true);
|
||||
Self::from_bits(BInt::from_bits(bits))
|
||||
};
|
||||
if new_result.[<wide_mul_ $n _ $n>](new_result)<=wide_self{
|
||||
result=new_result;
|
||||
result.as_bits_mut().as_bits_mut().set_bit(shift,true);
|
||||
if wide_self<result.[<wide_mul_ $n _ $n>](result){
|
||||
// put it back lol
|
||||
result.as_bits_mut().as_bits_mut().set_bit(shift,false);
|
||||
}
|
||||
}
|
||||
result
|
||||
|
@ -57,11 +57,11 @@ 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
|
||||
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.fix_2()).try_into();
|
||||
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.widen_2()).try_into();
|
||||
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
|
||||
let b:Result<I32F32,_>=f32::MIN_POSITIVE.try_into();
|
||||
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Underflow));
|
||||
@ -136,11 +136,24 @@ fn test_bint(){
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fix(){
|
||||
assert_eq!(I32F32::ONE.fix_8(),I256F256::ONE);
|
||||
assert_eq!(I32F32::ONE,I256F256::ONE.fix_1());
|
||||
assert_eq!(I32F32::NEG_ONE.fix_8(),I256F256::NEG_ONE);
|
||||
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.fix_1());
|
||||
fn test_wrap(){
|
||||
assert_eq!(I32F32::ONE,I256F256::ONE.wrap_1());
|
||||
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.wrap_1());
|
||||
}
|
||||
#[test]
|
||||
fn test_narrow(){
|
||||
assert_eq!(Ok(I32F32::ONE),I256F256::ONE.narrow_1());
|
||||
assert_eq!(Ok(I32F32::NEG_ONE),I256F256::NEG_ONE.narrow_1());
|
||||
}
|
||||
#[test]
|
||||
fn test_widen(){
|
||||
assert_eq!(I32F32::ONE.widen_8(),I256F256::ONE);
|
||||
assert_eq!(I32F32::NEG_ONE.widen_8(),I256F256::NEG_ONE);
|
||||
}
|
||||
#[test]
|
||||
fn test_clamp(){
|
||||
assert_eq!(I32F32::ONE,I256F256::ONE.clamp_1());
|
||||
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.clamp_1());
|
||||
}
|
||||
#[test]
|
||||
fn test_sqrt(){
|
||||
|
@ -15,8 +15,10 @@ macro_rules! impl_zeroes{
|
||||
let radicand=a1*a1-a2*a0*4;
|
||||
match radicand.cmp(&<Self as core::ops::Mul>::Output::ZERO){
|
||||
Ordering::Greater=>{
|
||||
// using wrap because sqrt always halves the number of leading digits.
|
||||
// clamp would be more defensive, but is slower.
|
||||
paste::item!{
|
||||
let planar_radicand=radicand.sqrt().[<fix_ $n>]();
|
||||
let planar_radicand=radicand.sqrt().[<wrap_ $n>]();
|
||||
}
|
||||
//sort roots ascending and avoid taking the difference of large numbers
|
||||
let zeroes=match (a2pos,Self::ZERO<a1){
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "linear_ops"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Vector/Matrix operations using trait bounds."
|
||||
@ -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.2.0", path = "../fixed_wide", registry = "strafesnet", optional = true }
|
||||
paste = { version = "1.0.15", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -38,40 +38,95 @@ macro_rules! impl_fixed_wide_vector {
|
||||
$crate::macro_4!(impl_fixed_wide_vector_not_const_generic,());
|
||||
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
|
||||
$crate::macro_repeated!(
|
||||
impl_fix_not_const_generic,(),
|
||||
(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),
|
||||
(1,2),(2,2),(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
|
||||
(1,3),(2,3),(3,3),(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
|
||||
(1,4),(2,4),(3,4),(4,4),(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),
|
||||
(1,5),(2,5),(3,5),(4,5),(5,5),(6,5),(7,5),(8,5),(9,5),(10,5),(11,5),(12,5),(13,5),(14,5),(15,5),(16,5),
|
||||
(1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6),(8,6),(9,6),(10,6),(11,6),(12,6),(13,6),(14,6),(15,6),(16,6),
|
||||
(1,7),(2,7),(3,7),(4,7),(5,7),(6,7),(7,7),(8,7),(9,7),(10,7),(11,7),(12,7),(13,7),(14,7),(15,7),(16,7),
|
||||
(1,8),(2,8),(3,8),(4,8),(5,8),(6,8),(7,8),(8,8),(9,8),(10,8),(11,8),(12,8),(13,8),(14,8),(15,8),(16,8),
|
||||
(1,9),(2,9),(3,9),(4,9),(5,9),(6,9),(7,9),(8,9),(9,9),(10,9),(11,9),(12,9),(13,9),(14,9),(15,9),(16,9),
|
||||
(1,10),(2,10),(3,10),(4,10),(5,10),(6,10),(7,10),(8,10),(9,10),(10,10),(11,10),(12,10),(13,10),(14,10),(15,10),(16,10),
|
||||
(1,11),(2,11),(3,11),(4,11),(5,11),(6,11),(7,11),(8,11),(9,11),(10,11),(11,11),(12,11),(13,11),(14,11),(15,11),(16,11),
|
||||
(1,12),(2,12),(3,12),(4,12),(5,12),(6,12),(7,12),(8,12),(9,12),(10,12),(11,12),(12,12),(13,12),(14,12),(15,12),(16,12),
|
||||
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),(13,13),(14,13),(15,13),(16,13),
|
||||
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),(14,14),(15,14),(16,14),
|
||||
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),(15,15),(16,15),
|
||||
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),(16,16)
|
||||
impl_narrow_not_const_generic,(),
|
||||
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
|
||||
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
|
||||
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
|
||||
(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),
|
||||
(6,5),(7,5),(8,5),(9,5),(10,5),(11,5),(12,5),(13,5),(14,5),(15,5),(16,5),
|
||||
(7,6),(8,6),(9,6),(10,6),(11,6),(12,6),(13,6),(14,6),(15,6),(16,6),
|
||||
(8,7),(9,7),(10,7),(11,7),(12,7),(13,7),(14,7),(15,7),(16,7),
|
||||
(9,8),(10,8),(11,8),(12,8),(13,8),(14,8),(15,8),(16,8),
|
||||
(10,9),(11,9),(12,9),(13,9),(14,9),(15,9),(16,9),
|
||||
(11,10),(12,10),(13,10),(14,10),(15,10),(16,10),
|
||||
(12,11),(13,11),(14,11),(15,11),(16,11),
|
||||
(13,12),(14,12),(15,12),(16,12),
|
||||
(14,13),(15,13),(16,13),
|
||||
(15,14),(16,14),
|
||||
(16,15)
|
||||
);
|
||||
$crate::macro_repeated!(
|
||||
impl_widen_not_const_generic,(),
|
||||
(1,2),
|
||||
(1,3),(2,3),
|
||||
(1,4),(2,4),(3,4),
|
||||
(1,5),(2,5),(3,5),(4,5),
|
||||
(1,6),(2,6),(3,6),(4,6),(5,6),
|
||||
(1,7),(2,7),(3,7),(4,7),(5,7),(6,7),
|
||||
(1,8),(2,8),(3,8),(4,8),(5,8),(6,8),(7,8),
|
||||
(1,9),(2,9),(3,9),(4,9),(5,9),(6,9),(7,9),(8,9),
|
||||
(1,10),(2,10),(3,10),(4,10),(5,10),(6,10),(7,10),(8,10),(9,10),
|
||||
(1,11),(2,11),(3,11),(4,11),(5,11),(6,11),(7,11),(8,11),(9,11),(10,11),
|
||||
(1,12),(2,12),(3,12),(4,12),(5,12),(6,12),(7,12),(8,12),(9,12),(10,12),(11,12),
|
||||
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
|
||||
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
|
||||
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
|
||||
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
|
||||
(1,17)
|
||||
);
|
||||
impl<const N:usize,T:fixed_wide::fixed::Wrap<U>,U> fixed_wide::fixed::Wrap<Vector<N,U>> for Vector<N,T>
|
||||
{
|
||||
#[inline]
|
||||
fn wrap(self)->Vector<N,U>{
|
||||
self.map(|t|t.wrap())
|
||||
}
|
||||
}
|
||||
impl<const N:usize,T:fixed_wide::fixed::Clamp<U>,U> fixed_wide::fixed::Clamp<Vector<N,U>> for Vector<N,T>
|
||||
{
|
||||
#[inline]
|
||||
fn clamp(self)->Vector<N,U>{
|
||||
self.map(|t|t.clamp())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! impl_fix_not_const_generic{
|
||||
macro_rules! impl_narrow_not_const_generic{
|
||||
(
|
||||
(),
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>
|
||||
{
|
||||
paste::item!{
|
||||
paste::item!{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
|
||||
#[inline]
|
||||
pub fn [<fix_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
self.map(|t|t.[<fix_ $rhs>]())
|
||||
pub fn [<wrap_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
self.map(|t|t.[<wrap_ $rhs>]())
|
||||
}
|
||||
#[inline]
|
||||
pub fn [<narrow_ $rhs>](self)->Vector<N,Result<fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>,fixed_wide::fixed::NarrowError>>{
|
||||
self.map(|t|t.[<narrow_ $rhs>]())
|
||||
}
|
||||
#[inline]
|
||||
pub fn [<clamp_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
self.map(|t|t.[<clamp_ $rhs>]())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! impl_widen_not_const_generic{
|
||||
(
|
||||
(),
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
paste::item!{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
|
||||
#[inline]
|
||||
pub fn [<widen_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
self.map(|t|t.[<widen_ $rhs>]())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,13 +204,19 @@ macro_rules! impl_matrix_named_fields_shape {
|
||||
type Target=$struct_outer<Vector<$size_inner,T>>;
|
||||
#[inline]
|
||||
fn deref(&self)->&Self::Target{
|
||||
unsafe{core::mem::transmute(&self.array)}
|
||||
// This cast is valid because Matrix has #[repr(transparent)]
|
||||
let ptr=&self.array as *const [[T;$size_inner];$size_outer] as *const Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&*ptr}
|
||||
}
|
||||
}
|
||||
impl<T> core::ops::DerefMut for Matrix<$size_outer,$size_inner,T>{
|
||||
#[inline]
|
||||
fn deref_mut(&mut self)->&mut Self::Target{
|
||||
unsafe{core::mem::transmute(&mut self.array)}
|
||||
// This cast is valid because Matrix has #[repr(transparent)]
|
||||
let ptr=&mut self.array as *mut [[T;$size_inner];$size_outer] as *mut Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&mut*ptr}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,15 @@ macro_rules! impl_vector {
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N:usize,T,E:std::fmt::Debug> Vector<N,Result<T,E>>{
|
||||
#[inline]
|
||||
pub fn unwrap(self)->Vector<N,T>{
|
||||
Vector{
|
||||
array:self.array.map(Result::unwrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N:usize,T:Ord> Vector<N,T>{
|
||||
#[inline]
|
||||
pub fn min(self,rhs:Self)->Self{
|
||||
@ -321,13 +330,19 @@ macro_rules! impl_vector_named_fields {
|
||||
type Target=$struct<T>;
|
||||
#[inline]
|
||||
fn deref(&self)->&Self::Target{
|
||||
unsafe{core::mem::transmute(&self.array)}
|
||||
// This cast is valid because Vector has #[repr(transparent)]
|
||||
let ptr=&self.array as *const [T;$size] as *const Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&*ptr}
|
||||
}
|
||||
}
|
||||
impl<T> core::ops::DerefMut for Vector<$size,T>{
|
||||
#[inline]
|
||||
fn deref_mut(&mut self)->&mut Self::Target{
|
||||
unsafe{core::mem::transmute(&mut self.array)}
|
||||
// This cast is valid because Vector has #[repr(transparent)]
|
||||
let ptr=&mut self.array as *mut [T;$size] as *mut Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&mut*ptr}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::vector::Vector;
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||
pub struct Matrix<const X:usize,const Y:usize,T>{
|
||||
pub(crate) array:[[T;Y];X],
|
||||
|
@ -3,6 +3,7 @@
|
||||
/// v.x += v.z;
|
||||
/// println!("v.x={}",v.x);
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||
pub struct Vector<const N:usize,T>{
|
||||
pub(crate) array:[T;N],
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "ratio_ops"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Ratio operations using trait bounds for avoiding division like the plague."
|
||||
|
@ -268,30 +268,35 @@ impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialEq<Ratio<RhsNum,RhsDen>> for Ratio<
|
||||
}
|
||||
impl<Num,Den> Eq for Ratio<Num,Den> where Self:PartialEq{}
|
||||
|
||||
// Wow! These were both completely wrong!
|
||||
// Idea: use a 'signed' trait instead of parity and float the sign to the numerator.
|
||||
impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialOrd<Ratio<RhsNum,RhsDen>> for Ratio<LhsNum,LhsDen>
|
||||
where
|
||||
LhsNum:Copy,
|
||||
LhsDen:Copy,
|
||||
LhsDen:Copy+Parity,
|
||||
RhsNum:Copy,
|
||||
RhsDen:Copy,
|
||||
RhsDen:Copy+Parity,
|
||||
LhsNum:core::ops::Mul<RhsDen,Output=T>,
|
||||
LhsDen:core::ops::Mul<RhsNum,Output=T>,
|
||||
RhsNum:core::ops::Mul<LhsDen,Output=U>,
|
||||
T:PartialOrd<U>,
|
||||
RhsDen:core::ops::Mul<LhsNum,Output=U>,
|
||||
T:PartialOrd<U>+Ord,
|
||||
{
|
||||
#[inline]
|
||||
fn partial_cmp(&self,other:&Ratio<RhsNum,RhsDen>)->Option<core::cmp::Ordering>{
|
||||
(self.num*other.den).partial_cmp(&(other.num*self.den))
|
||||
fn partial_cmp(&self,&other:&Ratio<RhsNum,RhsDen>)->Option<core::cmp::Ordering>{
|
||||
self.partial_cmp_ratio(other)
|
||||
}
|
||||
}
|
||||
impl<Num,Den,T> Ord for Ratio<Num,Den>
|
||||
where
|
||||
Num:Copy,
|
||||
Den:Copy,
|
||||
Den:Copy+Parity,
|
||||
Num:core::ops::Mul<Den,Output=T>,
|
||||
Den:core::ops::Mul<Num,Output=T>,
|
||||
T:Ord,
|
||||
{
|
||||
#[inline]
|
||||
fn cmp(&self,other:&Self)->std::cmp::Ordering{
|
||||
(self.num*other.den).cmp(&(other.num*self.den))
|
||||
fn cmp(&self,&other:&Self)->std::cmp::Ordering{
|
||||
self.cmp_ratio(other)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "strafesnet_rbx_loader"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Convert Roblox place and model files to StrafesNET data structures."
|
||||
@ -11,12 +11,14 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
bytemuck = "1.14.3"
|
||||
glam = "0.29.0"
|
||||
glam = "0.30.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_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" }
|
||||
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
|
||||
rbx_mesh = "0.3.1"
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
|
||||
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
|
||||
roblox_emulator = { version = "0.5.0", path = "../roblox_emulator", default-features = false, 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,12 @@
|
||||
use std::io::Read;
|
||||
use rbx_dom_weak::WeakDom;
|
||||
use roblox_emulator::context::Context;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
mod rbx;
|
||||
mod mesh;
|
||||
mod union;
|
||||
pub mod loader;
|
||||
mod primitives;
|
||||
|
||||
pub mod data{
|
||||
@ -24,11 +28,8 @@ impl Model{
|
||||
fn new(dom:WeakDom)->Self{
|
||||
Self{dom}
|
||||
}
|
||||
pub fn into_place(self)->Place{
|
||||
let Self{mut dom}=self;
|
||||
let context=roblox_emulator::context::Context::from_mut(&mut dom);
|
||||
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{
|
||||
@ -38,33 +39,41 @@ impl AsRef<WeakDom> for Model{
|
||||
}
|
||||
|
||||
pub struct Place{
|
||||
dom:WeakDom,
|
||||
services:roblox_emulator::context::Services,
|
||||
context:Context,
|
||||
}
|
||||
impl Place{
|
||||
fn new(dom:WeakDom)->Option<Self>{
|
||||
let context=roblox_emulator::context::Context::from_ref(&dom);
|
||||
Some(Self{
|
||||
services:context.find_services()?,
|
||||
dom,
|
||||
pub fn new(dom:WeakDom)->Result<Self,roblox_emulator::context::ServicesError>{
|
||||
let context=Context::from_place(dom)?;
|
||||
Ok(Self{
|
||||
context,
|
||||
})
|
||||
}
|
||||
pub fn run_scripts(&mut self){
|
||||
let Place{dom,services}=self;
|
||||
let Place{context}=self;
|
||||
let runner=roblox_emulator::runner::Runner::new().unwrap();
|
||||
let context=roblox_emulator::context::Context::from_mut(dom);
|
||||
let scripts=context.scripts();
|
||||
let runnable=runner.runnable_context_with_services(context,services).unwrap();
|
||||
let runnable=runner.runnable_context(context).unwrap();
|
||||
for script in scripts{
|
||||
if let Err(e)=runnable.run_script(script){
|
||||
println!("runner error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
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{
|
||||
&self.dom
|
||||
self.context.as_ref()
|
||||
}
|
||||
}
|
||||
impl From<Model> for Place{
|
||||
fn from(model:Model)->Self{
|
||||
let context=Context::from_model(model.dom);
|
||||
Self{
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,23 +94,56 @@ impl std::error::Error for ReadError{}
|
||||
pub fn read<R:Read>(input:R)->Result<Model,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..8]{
|
||||
b"<roblox!"=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
|
||||
b"<roblox "=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
|
||||
match peek.get(0..8){
|
||||
Some(b"<roblox!")=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
|
||||
Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
|
||||
_=>Err(ReadError::UnknownFileFormat),
|
||||
}
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
196
lib/rbx_loader/src/loader.rs
Normal file
196
lib/rbx_loader/src/loader.rs
Normal file
@ -0,0 +1,196 @@
|
||||
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::RobloxPartDescription;
|
||||
|
||||
// disallow non-static lifetimes
|
||||
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
|
||||
rbx_dom_weak::ustr(s)
|
||||
}
|
||||
|
||||
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;
|
||||
impl TextureLoader{
|
||||
pub fn new()->Self{
|
||||
Self
|
||||
}
|
||||
}
|
||||
impl Loader for TextureLoader{
|
||||
type Error=TextureError;
|
||||
type Index<'a>=&'a str;
|
||||
type Resource=Texture;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->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:RobloxPartDescription,
|
||||
},
|
||||
}
|
||||
#[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: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;
|
||||
impl MeshLoader{
|
||||
pub fn new()->Self{
|
||||
Self
|
||||
}
|
||||
}
|
||||
impl Loader for MeshLoader{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=MeshIndex<'a>;
|
||||
type Resource=Mesh;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->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(&static_ustr("PhysicsData")){
|
||||
physics_data=data.as_ref();
|
||||
}
|
||||
}
|
||||
if mesh_data.is_empty(){
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("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 rbx_mesh::mesh::{Vertex2,Vertex2Truncated};
|
||||
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),
|
||||
@ -83,13 +84,13 @@ where
|
||||
fn ingest_faces2_lods3(
|
||||
polygon_groups:&mut Vec<PolygonGroup>,
|
||||
vertex_id_map:&HashMap<rbx_mesh::mesh::VertexId2,VertexId>,
|
||||
faces:&Vec<rbx_mesh::mesh::Face2>,
|
||||
lods:&Vec<rbx_mesh::mesh::Lod3>
|
||||
faces:&[rbx_mesh::mesh::Face2],
|
||||
lods:&[rbx_mesh::mesh::Lod3],
|
||||
){
|
||||
//faces have to be split into polygon groups based on lod
|
||||
polygon_groups.extend(lods.windows(2).map(|lod_pair|
|
||||
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|face|
|
||||
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
|
||||
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
|
||||
vec![vertex_id_map[&v0],vertex_id_map[&v1],vertex_id_map[&v2]]
|
||||
).collect()))
|
||||
))
|
||||
}
|
||||
@ -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,5 +1,6 @@
|
||||
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,IndexedVertexList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
|
||||
use strafesnet_common::integer::{vec3,Planar64Vec3};
|
||||
use crate::rbx::{RobloxPartDescription,RobloxWedgeDescription,RobloxCornerWedgeDescription};
|
||||
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,MeshBuilder,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
|
||||
use strafesnet_common::integer::{vec3,Planar64,Planar64Vec3};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Primitives{
|
||||
@ -9,7 +10,22 @@ pub enum Primitives{
|
||||
Wedge,
|
||||
CornerWedge,
|
||||
}
|
||||
#[derive(Hash,PartialEq,Eq)]
|
||||
#[derive(Debug)]
|
||||
pub struct PrimitivesError;
|
||||
impl TryFrom<u32> for Primitives{
|
||||
type Error=PrimitivesError;
|
||||
fn try_from(value:u32)->Result<Self,Self::Error>{
|
||||
match value{
|
||||
0=>Ok(Primitives::Sphere),
|
||||
1=>Ok(Primitives::Cube),
|
||||
2=>Ok(Primitives::Cylinder),
|
||||
3=>Ok(Primitives::Wedge),
|
||||
4=>Ok(Primitives::CornerWedge),
|
||||
_=>Err(PrimitivesError),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
|
||||
pub enum CubeFace{
|
||||
Right,
|
||||
Top,
|
||||
@ -18,6 +34,22 @@ pub enum CubeFace{
|
||||
Bottom,
|
||||
Front,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct CubeFaceError;
|
||||
impl TryFrom<u32> for CubeFace{
|
||||
type Error=CubeFaceError;
|
||||
fn try_from(value:u32)->Result<Self,Self::Error>{
|
||||
match value{
|
||||
0=>Ok(CubeFace::Right),
|
||||
1=>Ok(CubeFace::Top),
|
||||
2=>Ok(CubeFace::Back),
|
||||
3=>Ok(CubeFace::Left),
|
||||
4=>Ok(CubeFace::Bottom),
|
||||
5=>Ok(CubeFace::Front),
|
||||
_=>Err(CubeFaceError),
|
||||
}
|
||||
}
|
||||
}
|
||||
const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[
|
||||
TextureCoordinate::new(0.0,0.0),
|
||||
TextureCoordinate::new(1.0,0.0),
|
||||
@ -42,154 +74,36 @@ const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
|
||||
vec3::int( 0,-1, 0),//CubeFace::Bottom
|
||||
vec3::int( 0, 0,-1),//CubeFace::Front
|
||||
];
|
||||
const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
|
||||
// right (1, 0, 0)
|
||||
[
|
||||
[6,2,0],//[vertex,tex,norm]
|
||||
[5,1,0],
|
||||
[2,0,0],
|
||||
[1,3,0],
|
||||
],
|
||||
// top (0, 1, 0)
|
||||
[
|
||||
[5,3,1],
|
||||
[4,2,1],
|
||||
[3,1,1],
|
||||
[2,0,1],
|
||||
],
|
||||
// back (0, 0, 1)
|
||||
[
|
||||
[0,3,2],
|
||||
[1,2,2],
|
||||
[2,1,2],
|
||||
[3,0,2],
|
||||
],
|
||||
// left (-1, 0, 0)
|
||||
[
|
||||
[0,2,3],
|
||||
[3,1,3],
|
||||
[4,0,3],
|
||||
[7,3,3],
|
||||
],
|
||||
// bottom (0,-1, 0)
|
||||
[
|
||||
[1,1,4],
|
||||
[0,0,4],
|
||||
[7,3,4],
|
||||
[6,2,4],
|
||||
],
|
||||
// front (0, 0,-1)
|
||||
[
|
||||
[4,1,5],
|
||||
[5,0,5],
|
||||
[6,3,5],
|
||||
[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]);
|
||||
pub struct CubeFaceDescription([FaceDescription;Self::FACES]);
|
||||
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 const FACES:usize=6;
|
||||
pub fn new(RobloxPartDescription(part_description):RobloxPartDescription,textureless_render_id:RenderConfigId)->Self{
|
||||
Self(part_description.map(|face_description|match face_description{
|
||||
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
|
||||
None=>FaceDescription::new_with_render_id(textureless_render_id),
|
||||
}))
|
||||
}
|
||||
}
|
||||
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]);
|
||||
pub struct WedgeFaceDescription([FaceDescription;Self::FACES]);
|
||||
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 const FACES:usize=5;
|
||||
pub fn new(RobloxWedgeDescription(part_description):RobloxWedgeDescription,textureless_render_id:RenderConfigId)->Self{
|
||||
Self(part_description.map(|face_description|match face_description{
|
||||
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
|
||||
None=>FaceDescription::new_with_render_id(textureless_render_id),
|
||||
}))
|
||||
}
|
||||
}
|
||||
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]);
|
||||
pub struct CornerWedgeFaceDescription([FaceDescription;Self::FACES]);
|
||||
impl CornerWedgeFaceDescription{
|
||||
pub fn insert(&mut self,index:CornerWedgeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
pub const FACES:usize=5;
|
||||
pub fn new(RobloxCornerWedgeDescription(part_description):RobloxCornerWedgeDescription,textureless_render_id:RenderConfigId)->Self{
|
||||
Self(part_description.map(|face_description|match face_description{
|
||||
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
|
||||
None=>FaceDescription::new_with_render_id(textureless_render_id),
|
||||
}))
|
||||
}
|
||||
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)]
|
||||
@ -199,7 +113,7 @@ pub struct FaceDescription{
|
||||
pub color:Color4,
|
||||
}
|
||||
impl FaceDescription{
|
||||
pub fn new_with_render_id(render:RenderConfigId)->Self {
|
||||
pub fn new_with_render_id(render:RenderConfigId)->Self{
|
||||
Self{
|
||||
render,
|
||||
transform:glam::Affine2::IDENTITY,
|
||||
@ -207,7 +121,51 @@ impl FaceDescription{
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Mesh{
|
||||
const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[
|
||||
// right (1, 0, 0)
|
||||
[
|
||||
[6,2],//[vertex,tex]
|
||||
[5,1],
|
||||
[2,0],
|
||||
[1,3],
|
||||
],
|
||||
// top (0, 1, 0)
|
||||
[
|
||||
[5,3],
|
||||
[4,2],
|
||||
[3,1],
|
||||
[2,0],
|
||||
],
|
||||
// back (0, 0, 1)
|
||||
[
|
||||
[0,3],
|
||||
[1,2],
|
||||
[2,1],
|
||||
[3,0],
|
||||
],
|
||||
// left (-1, 0, 0)
|
||||
[
|
||||
[0,2],
|
||||
[3,1],
|
||||
[4,0],
|
||||
[7,3],
|
||||
],
|
||||
// bottom (0,-1, 0)
|
||||
[
|
||||
[1,1],
|
||||
[0,0],
|
||||
[7,3],
|
||||
[6,2],
|
||||
],
|
||||
// front (0, 0,-1)
|
||||
[
|
||||
[4,1],
|
||||
[5,0],
|
||||
[6,3],
|
||||
[7,2],
|
||||
],
|
||||
];
|
||||
let mut generated_pos=Vec::new();
|
||||
let mut generated_tex=Vec::new();
|
||||
let mut generated_normal=Vec::new();
|
||||
@ -218,7 +176,7 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
let mut physics_group=IndexedPhysicsGroup::default();
|
||||
let mut transforms=Vec::new();
|
||||
//note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices.
|
||||
for (face_id,face_description) in face_descriptions.pairs(){
|
||||
for (face_id,face_description) in face_descriptions.into_iter().enumerate(){
|
||||
//assume that scanning short lists is faster than hashing.
|
||||
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
|
||||
transform_index
|
||||
@ -245,8 +203,8 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->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![
|
||||
CUBE_DEFAULT_POLYS[face_id].map(|tup|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
|
||||
CUBE_DEFAULT_POLYS[face_id].map(|[pos_id,tex_id]|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize];
|
||||
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
|
||||
pos_index
|
||||
}else{
|
||||
@ -258,7 +216,7 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
//always push vertex
|
||||
let vertex=IndexedVertex{
|
||||
pos:PositionId::new(pos_index),
|
||||
tex:TextureCoordinateId::new(tup[1]+4*transform_index),
|
||||
tex:TextureCoordinateId::new(tex_id+4*transform_index),
|
||||
normal:NormalId::new(normal_index),
|
||||
color:ColorId::new(color_index),
|
||||
};
|
||||
@ -285,42 +243,49 @@ 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=[
|
||||
pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)->Mesh{
|
||||
const WEDGE_DEFAULT_POLYS:[&[[u32;2]];5]=[
|
||||
// right (1, 0, 0)
|
||||
vec![
|
||||
[6,2,0],//[vertex,tex,norm]
|
||||
[2,0,0],
|
||||
[1,3,0],
|
||||
&[
|
||||
[6,2],//[vertex,tex]
|
||||
[2,0],
|
||||
[1,3],
|
||||
],
|
||||
// FrontTop (0, 1, -1)
|
||||
vec![
|
||||
[3,1,1],
|
||||
[2,0,1],
|
||||
[6,3,1],
|
||||
[7,2,1],
|
||||
&[
|
||||
[3,1],
|
||||
[2,0],
|
||||
[6,3],
|
||||
[7,2],
|
||||
],
|
||||
// back (0, 0, 1)
|
||||
vec![
|
||||
[0,3,2],
|
||||
[1,2,2],
|
||||
[2,1,2],
|
||||
[3,0,2],
|
||||
&[
|
||||
[0,3],
|
||||
[1,2],
|
||||
[2,1],
|
||||
[3,0],
|
||||
],
|
||||
// left (-1, 0, 0)
|
||||
vec![
|
||||
[0,2,3],
|
||||
[3,1,3],
|
||||
[7,3,3],
|
||||
&[
|
||||
[0,2],
|
||||
[3,1],
|
||||
[7,3],
|
||||
],
|
||||
// bottom (0,-1, 0)
|
||||
vec![
|
||||
[1,1,4],
|
||||
[0,0,4],
|
||||
[7,3,4],
|
||||
[6,2,4],
|
||||
&[
|
||||
[1,1],
|
||||
[0,0],
|
||||
[7,3],
|
||||
[6,2],
|
||||
],
|
||||
];
|
||||
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
|
||||
];
|
||||
let mut generated_pos=Vec::new();
|
||||
let mut generated_tex=Vec::new();
|
||||
let mut generated_normal=Vec::new();
|
||||
@ -331,7 +296,7 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
|
||||
let mut physics_group=IndexedPhysicsGroup::default();
|
||||
let mut transforms=Vec::new();
|
||||
//note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices.
|
||||
for (face_id,face_description) in face_descriptions.pairs(){
|
||||
for (face_id,face_description) in face_descriptions.into_iter().enumerate(){
|
||||
//assume that scanning short lists is faster than hashing.
|
||||
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
|
||||
transform_index
|
||||
@ -358,8 +323,8 @@ 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|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
|
||||
WEDGE_DEFAULT_POLYS[face_id].iter().map(|&[pos_id,tex_id]|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize];
|
||||
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
|
||||
pos_index
|
||||
}else{
|
||||
@ -371,7 +336,7 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
|
||||
//always push vertex
|
||||
let vertex=IndexedVertex{
|
||||
pos:PositionId::new(pos_index),
|
||||
tex:TextureCoordinateId::new(tup[1]+4*transform_index),
|
||||
tex:TextureCoordinateId::new(tex_id+4*transform_index),
|
||||
normal:NormalId::new(normal_index),
|
||||
color:ColorId::new(color_index),
|
||||
};
|
||||
@ -398,40 +363,47 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{
|
||||
let cornerwedge_default_polys=[
|
||||
pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedgeFaceDescription)->Mesh{
|
||||
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;2]];5]=[
|
||||
// right (1, 0, 0)
|
||||
vec![
|
||||
[6,2,0],//[vertex,tex,norm]
|
||||
[5,1,0],
|
||||
[1,3,0],
|
||||
&[
|
||||
[6,2],//[vertex,tex]
|
||||
[5,1],
|
||||
[1,3],
|
||||
],
|
||||
// BackTop (0, 1, 1)
|
||||
vec![
|
||||
[5,3,1],
|
||||
[0,1,1],
|
||||
[1,0,1],
|
||||
&[
|
||||
[5,3],
|
||||
[0,1],
|
||||
[1,0],
|
||||
],
|
||||
// LeftTop (-1, 1, 0)
|
||||
vec![
|
||||
[5,3,2],
|
||||
[7,2,2],
|
||||
[0,1,2],
|
||||
&[
|
||||
[5,3],
|
||||
[7,2],
|
||||
[0,1],
|
||||
],
|
||||
// bottom (0,-1, 0)
|
||||
vec![
|
||||
[1,1,3],
|
||||
[0,0,3],
|
||||
[7,3,3],
|
||||
[6,2,3],
|
||||
&[
|
||||
[1,1],
|
||||
[0,0],
|
||||
[7,3],
|
||||
[6,2],
|
||||
],
|
||||
// front (0, 0,-1)
|
||||
vec![
|
||||
[5,0,4],
|
||||
[6,3,4],
|
||||
[7,2,4],
|
||||
&[
|
||||
[5,0],
|
||||
[6,3],
|
||||
[7,2],
|
||||
],
|
||||
];
|
||||
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
|
||||
];
|
||||
let mut generated_pos=Vec::new();
|
||||
let mut generated_tex=Vec::new();
|
||||
let mut generated_normal=Vec::new();
|
||||
@ -442,7 +414,7 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
|
||||
let mut physics_group=IndexedPhysicsGroup::default();
|
||||
let mut transforms=Vec::new();
|
||||
//note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices.
|
||||
for (face_id,face_description) in face_descriptions.pairs(){
|
||||
for (face_id,face_description) in face_descriptions.into_iter().enumerate(){
|
||||
//assume that scanning short lists is faster than hashing.
|
||||
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
|
||||
transform_index
|
||||
@ -469,8 +441,8 @@ 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|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
|
||||
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|&[pos_id,tex_id]|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize];
|
||||
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
|
||||
pos_index
|
||||
}else{
|
||||
@ -482,7 +454,7 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
|
||||
//always push vertex
|
||||
let vertex=IndexedVertex{
|
||||
pos:PositionId::new(pos_index),
|
||||
tex:TextureCoordinateId::new(tup[1]+4*transform_index),
|
||||
tex:TextureCoordinateId::new(tex_id+4*transform_index),
|
||||
normal:NormalId::new(normal_index),
|
||||
color:ColorId::new(color_index),
|
||||
};
|
||||
@ -508,3 +480,133 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
|
||||
physics_groups:vec![physics_group],
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix face texture orientation
|
||||
pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
// cylinder is oriented about the x axis
|
||||
// roblox cylinders use projected grid coordinates
|
||||
/// how many grid coordinates to use (positive and negative)
|
||||
const GON:i32=3;
|
||||
/// grid perimeter
|
||||
const POINTS:[[i32;2];4*2*GON as usize]=const{
|
||||
let mut points=[[0;2];{4*2*GON as usize}];
|
||||
let mut i=-GON;
|
||||
while i<GON{
|
||||
points[(i+GON) as usize]=[i,GON];
|
||||
points[(i+GON+1*2*GON) as usize]=[GON,-i];
|
||||
points[(i+GON+2*2*GON) as usize]=[-i,-GON];
|
||||
points[(i+GON+3*2*GON) as usize]=[-GON,i];
|
||||
i+=1;
|
||||
}
|
||||
points
|
||||
};
|
||||
|
||||
let mut mb=MeshBuilder::new();
|
||||
let mut polygon_groups=Vec::with_capacity(CubeFaceDescription::FACES);
|
||||
let mut graphics_groups=Vec::with_capacity(CubeFaceDescription::FACES);
|
||||
let mut physics_group=IndexedPhysicsGroup{groups:Vec::with_capacity(CubeFaceDescription::FACES)};
|
||||
let CubeFaceDescription([right,top,back,left,bottom,front])=face_descriptions;
|
||||
|
||||
macro_rules! end_face{
|
||||
($face_description:expr,$end:expr,$iter:expr)=>{
|
||||
let normal=mb.acquire_normal_id($end);
|
||||
let color=mb.acquire_color_id($face_description.color);
|
||||
|
||||
// single polygon for physics
|
||||
let polygon:Vec<_>=$iter.map(|[x,y]|{
|
||||
let tex=mb.acquire_tex_id(
|
||||
$face_description.transform.transform_point2(
|
||||
(glam::vec2(-x as f32,y as f32).normalize()+1.0)/2.0
|
||||
)
|
||||
);
|
||||
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_1());
|
||||
mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})
|
||||
}).collect();
|
||||
|
||||
// fanned polygons for graphics
|
||||
let pos=mb.acquire_pos_id($end);
|
||||
let tex=mb.acquire_tex_id($face_description.transform.transform_point2(glam::Vec2::ONE/2.0));
|
||||
let center=mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color});
|
||||
let polygon_list=(0..POINTS.len()).map(|i|
|
||||
vec![center,polygon[i],polygon[(i+1)%POINTS.len()]]
|
||||
).collect();
|
||||
|
||||
// end face graphics
|
||||
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
|
||||
graphics_groups.push(IndexedGraphicsGroup{
|
||||
render:$face_description.render,
|
||||
groups:vec![group_id],
|
||||
});
|
||||
|
||||
// end face physics
|
||||
let polygon_list=vec![polygon];
|
||||
|
||||
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
|
||||
physics_group.groups.push(group_id);
|
||||
}
|
||||
}
|
||||
macro_rules! tex{
|
||||
($face_description:expr,$tex:expr)=>{{
|
||||
let [x,y]=$tex;
|
||||
$face_description.transform.transform_point2(
|
||||
glam::vec2((x+GON) as f32,(y+GON) as f32)/(2*GON) as f32
|
||||
)
|
||||
}};
|
||||
}
|
||||
macro_rules! barrel_face{
|
||||
($face_description:expr,$loop:ident,$lo_dir:expr,$hi_dir:expr,$tex_0:expr,$tex_1:expr,$tex_2:expr,$tex_3:expr)=>{
|
||||
let mut polygon_list=Vec::with_capacity(CubeFaceDescription::FACES);
|
||||
for $loop in -GON..GON{
|
||||
// lo Z
|
||||
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_1();
|
||||
// hi Z
|
||||
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_1();
|
||||
|
||||
// pos
|
||||
let lx_lz_pos=mb.acquire_pos_id(vec3::NEG_X+lz_dir);
|
||||
let lx_hz_pos=mb.acquire_pos_id(vec3::NEG_X+hz_dir);
|
||||
let hx_hz_pos=mb.acquire_pos_id(vec3::X+hz_dir);
|
||||
let hx_lz_pos=mb.acquire_pos_id(vec3::X+lz_dir);
|
||||
// tex
|
||||
let lx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_0));
|
||||
let lx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_1));
|
||||
let hx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_2));
|
||||
let hx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_3));
|
||||
// norm
|
||||
let lz_norm=mb.acquire_normal_id(lz_dir);
|
||||
let hz_norm=mb.acquire_normal_id(hz_dir);
|
||||
// color
|
||||
let color=mb.acquire_color_id($face_description.color);
|
||||
|
||||
polygon_list.push(vec![
|
||||
mb.acquire_vertex_id(IndexedVertex{pos:lx_lz_pos,tex:lx_lz_tex,normal:lz_norm,color}),
|
||||
mb.acquire_vertex_id(IndexedVertex{pos:lx_hz_pos,tex:lx_hz_tex,normal:hz_norm,color}),
|
||||
mb.acquire_vertex_id(IndexedVertex{pos:hx_hz_pos,tex:hx_hz_tex,normal:hz_norm,color}),
|
||||
mb.acquire_vertex_id(IndexedVertex{pos:hx_lz_pos,tex:hx_lz_tex,normal:lz_norm,color}),
|
||||
]);
|
||||
}
|
||||
|
||||
// push face
|
||||
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
|
||||
graphics_groups.push(IndexedGraphicsGroup{
|
||||
render:$face_description.render,
|
||||
groups:vec![group_id],
|
||||
});
|
||||
physics_group.groups.push(group_id);
|
||||
};
|
||||
}
|
||||
|
||||
end_face!(right, vec3::X,POINTS.into_iter());
|
||||
barrel_face!(top, z,vec3::int(0,GON,z),vec3::int(0,GON,z+1), [GON,z],[GON,z+1],[-GON,z+1],[-GON,z]);
|
||||
barrel_face!(back, y,vec3::int(0,y+1,GON),vec3::int(0,y,GON), [GON,y+1],[GON,y],[-GON,y],[-GON,y+1]);
|
||||
end_face!(left, vec3::NEG_X,POINTS.into_iter().rev());
|
||||
barrel_face!(bottom, z,vec3::int(0,-GON,z+1),vec3::int(0,-GON,z), [-GON,z+1],[-GON,z],[GON,z],[GON,z+1]);
|
||||
barrel_face!(front, y,vec3::int(0,y,-GON),vec3::int(0,y+1,-GON), [-GON,y],[-GON,y+1],[GON,y+1],[GON,y]);
|
||||
|
||||
let physics_groups=vec![physics_group];
|
||||
|
||||
mb.build(polygon_groups,graphics_groups,physics_groups)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
178
lib/rbx_loader/src/union.rs
Normal file
178
lib/rbx_loader/src/union.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use rbx_mesh::mesh_data::{NormalId2 as MeshDataNormalId2,VertexId as MeshDataVertexId};
|
||||
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
|
||||
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,
|
||||
crate::rbx::RobloxPartDescription(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 [MeshDataVertexId(vertex_id0),MeshDataVertexId(vertex_id1),MeshDataVertexId(vertex_id2)] in graphics_mesh.faces{
|
||||
let face=[
|
||||
graphics_mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
|
||||
graphics_mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
|
||||
graphics_mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
|
||||
];
|
||||
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(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
|
||||
let face=[
|
||||
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
|
||||
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
|
||||
mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
|
||||
].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 = "2024"
|
||||
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();
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "roblox_emulator"
|
||||
version = "0.4.7"
|
||||
edition = "2021"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Run embedded Luau scripts which manipulate the DOM."
|
||||
@ -12,10 +12,10 @@ default=["run-service"]
|
||||
run-service=[]
|
||||
|
||||
[dependencies]
|
||||
glam = "0.29.0"
|
||||
glam = "0.30.0"
|
||||
mlua = { version = "0.10.1", features = ["luau"] }
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
|
||||
rbx_reflection = { version = "4.7.0", registry = "strafesnet" }
|
||||
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
|
||||
rbx_types = { version = "1.10.0", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
|
||||
rbx_reflection = "5.0.0"
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_types = "2.0.0"
|
||||
|
@ -1,93 +1,113 @@
|
||||
use crate::util::static_ustr;
|
||||
use rbx_dom_weak::{types::Ref,InstanceBuilder,WeakDom};
|
||||
|
||||
pub fn class_is_a(class:&str,superclass:&str)->bool{
|
||||
class==superclass
|
||||
||rbx_reflection_database::get().classes.get(class)
|
||||
.is_some_and(|descriptor|
|
||||
descriptor.superclass.as_ref().is_some_and(|class_super|
|
||||
class_is_a(class_super,superclass)
|
||||
)
|
||||
)
|
||||
#[derive(Debug)]
|
||||
pub enum ServicesError{
|
||||
WorkspaceNotFound,
|
||||
}
|
||||
impl std::fmt::Display for ServicesError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ServicesError{}
|
||||
|
||||
pub struct Services{
|
||||
pub(crate) game:Ref,
|
||||
pub(crate) workspace:Ref,
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
impl Services{
|
||||
fn find_services(dom:&WeakDom)->Result<Services,ServicesError>{
|
||||
Ok(Services{
|
||||
workspace:*dom.root().children().iter().find(|&&r|
|
||||
dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace")
|
||||
).ok_or(ServicesError::WorkspaceNotFound)?,
|
||||
game:dom.root_ref(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type LuaAppData=&'static mut WeakDom;
|
||||
pub struct Context{
|
||||
pub(crate)dom:WeakDom,
|
||||
pub(crate)services:Services,
|
||||
}
|
||||
|
||||
impl Context{
|
||||
pub const fn new(dom:WeakDom)->Self{
|
||||
Self{dom}
|
||||
pub fn from_place(dom:WeakDom)->Result<Context,ServicesError>{
|
||||
let services=Services::find_services(&dom)?;
|
||||
Ok(Self{dom,services})
|
||||
}
|
||||
pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance,Services){
|
||||
pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance){
|
||||
let script=InstanceBuilder::new("Script")
|
||||
.with_property("Source",rbx_types::Variant::String(source));
|
||||
let script_ref=script.referent();
|
||||
let mut context=Self::new(WeakDom::new(
|
||||
let dom=WeakDom::new(
|
||||
InstanceBuilder::new("DataModel")
|
||||
.with_child(script)
|
||||
));
|
||||
let services=context.convert_into_place();
|
||||
(context,crate::runner::instance::Instance::new(script_ref),services)
|
||||
}
|
||||
pub fn from_ref(dom:&WeakDom)->&Context{
|
||||
unsafe{&*(dom as *const WeakDom as *const Context)}
|
||||
}
|
||||
pub fn from_mut(dom:&mut WeakDom)->&mut Context{
|
||||
unsafe{&mut *(dom as *mut WeakDom as *mut Context)}
|
||||
);
|
||||
let context=Self::from_model(dom);
|
||||
(context,crate::runner::instance::Instance::new_unchecked(script_ref))
|
||||
}
|
||||
/// Creates an iterator over all items of a particular class.
|
||||
pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{
|
||||
self.dom.descendants().filter(|&instance|
|
||||
class_is_a(instance.class.as_ref(),superclass)
|
||||
).map(|instance|instance.referent())
|
||||
}
|
||||
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
|
||||
self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::Instance::new).collect()
|
||||
}
|
||||
|
||||
pub fn find_services(&self)->Option<Services>{
|
||||
Some(Services{
|
||||
workspace:*self.dom.root().children().iter().find(|&&r|
|
||||
self.dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace")
|
||||
)?,
|
||||
game:self.dom.root_ref(),
|
||||
let db=rbx_reflection_database::get();
|
||||
let Some(superclass)=db.classes.get(superclass)else{
|
||||
panic!("Invalid class");
|
||||
};
|
||||
self.dom.descendants().filter_map(|instance|{
|
||||
let class=db.classes.get(instance.class.as_str())?;
|
||||
db.has_superclass(class,superclass).then(||instance.referent())
|
||||
})
|
||||
}
|
||||
pub fn convert_into_place(&mut self)->Services{
|
||||
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
|
||||
self.superclass_iter("Script")
|
||||
.filter_map(|script_ref|{
|
||||
let script=self.dom.get_by_ref(script_ref)?;
|
||||
if let None|Some(rbx_dom_weak::types::Variant::Bool(false))=script.properties.get(&static_ustr("Disabled")){
|
||||
return Some(crate::runner::instance::Instance::new_unchecked(script_ref));
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn from_model(mut dom:WeakDom)->Context{
|
||||
//snapshot root instances
|
||||
let children=self.dom.root().children().to_owned();
|
||||
let children=dom.root().children().to_owned();
|
||||
|
||||
//insert services
|
||||
let game=self.dom.root_ref();
|
||||
let game=dom.root_ref();
|
||||
let terrain_bldr=InstanceBuilder::new("Terrain");
|
||||
let workspace=self.dom.insert(game,
|
||||
let workspace=dom.insert(game,
|
||||
InstanceBuilder::new("Workspace")
|
||||
//Set Workspace.Terrain property equal to Terrain
|
||||
.with_property("Terrain",terrain_bldr.referent())
|
||||
.with_child(terrain_bldr)
|
||||
);
|
||||
{
|
||||
//Lowercase and upper case workspace property!
|
||||
let game=self.dom.root_mut();
|
||||
game.properties.insert("workspace".to_owned(),rbx_types::Variant::Ref(workspace));
|
||||
game.properties.insert("Workspace".to_owned(),rbx_types::Variant::Ref(workspace));
|
||||
}
|
||||
self.dom.insert(game,InstanceBuilder::new("Lighting"));
|
||||
|
||||
//transfer original root instances into workspace
|
||||
for instance in children{
|
||||
self.dom.transfer_within(instance,workspace);
|
||||
dom.transfer_within(instance,workspace);
|
||||
}
|
||||
|
||||
Services{
|
||||
game,
|
||||
workspace,
|
||||
{
|
||||
//Lowercase and upper case workspace property!
|
||||
let game=dom.root_mut();
|
||||
// TODO: DELETE THIS!
|
||||
game.properties.insert(static_ustr("workspace"),rbx_types::Variant::Ref(workspace));
|
||||
game.properties.insert(static_ustr("Workspace"),rbx_types::Variant::Ref(workspace));
|
||||
}
|
||||
dom.insert(game,InstanceBuilder::new("Lighting"));
|
||||
|
||||
let services=Services{game,workspace};
|
||||
Self{dom,services}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Services{
|
||||
pub game:Ref,
|
||||
pub workspace:Ref,
|
||||
impl AsRef<WeakDom> for Context{
|
||||
fn as_ref(&self)->&WeakDom{
|
||||
&self.dom
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod util;
|
||||
pub mod runner;
|
||||
pub mod context;
|
||||
#[cfg(feature="run-service")]
|
||||
@ -5,3 +6,7 @@ pub(crate) mod scheduler;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod mlua{
|
||||
pub use mlua::{Result,Error};
|
||||
}
|
||||
|
59
lib/roblox_emulator/src/runner/brickcolor.rs
Normal file
59
lib/roblox_emulator/src/runner/brickcolor.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use super::color3::Color3;
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct BrickColor(rbx_types::BrickColor);
|
||||
impl BrickColor{
|
||||
pub fn from_name(name:&str)->Option<Self>{
|
||||
Some(BrickColor(rbx_types::BrickColor::from_name(name)?))
|
||||
}
|
||||
pub fn from_number(number:u16)->Option<Self>{
|
||||
Some(BrickColor(rbx_types::BrickColor::from_number(number)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
let table=lua.create_table()?;
|
||||
|
||||
table.raw_set("new",
|
||||
lua.create_function(|_,r:mlua::Value|
|
||||
match r{
|
||||
mlua::Value::String(name)=>Ok(BrickColor::from_name(&*name.to_str()?)),
|
||||
mlua::Value::Integer(number)=>Ok(BrickColor::from_number(number as u16)),
|
||||
_=>Err(mlua::Error::runtime("Unsupported arguments"))
|
||||
}
|
||||
|
||||
)?
|
||||
)?;
|
||||
|
||||
macro_rules! brickcolor_constructor{
|
||||
($fname:expr,$internal:ident)=>{
|
||||
table.raw_set($fname,
|
||||
lua.create_function(|_,_:()|
|
||||
Ok(BrickColor(rbx_types::BrickColor::$internal))
|
||||
)?
|
||||
)?;
|
||||
};
|
||||
}
|
||||
brickcolor_constructor!("White",White);
|
||||
brickcolor_constructor!("Gray",MediumStoneGrey);
|
||||
brickcolor_constructor!("DarkGray",DarkStoneGrey);
|
||||
brickcolor_constructor!("Black",Black);
|
||||
brickcolor_constructor!("Red",BrightRed);
|
||||
brickcolor_constructor!("Yellow",BrightYellow);
|
||||
brickcolor_constructor!("Green",DarkGreen);
|
||||
brickcolor_constructor!("Blue",BrightBlue);
|
||||
|
||||
globals.set("BrickColor",table)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl mlua::UserData for BrickColor{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
|
||||
fields.add_field_method_get("Color",|_,BrickColor(this)|{
|
||||
let rbx_types::Color3uint8{r,g,b}=this.to_color3uint8();
|
||||
Ok(Color3::from_rgb(r,g,b))
|
||||
});
|
||||
}
|
||||
}
|
||||
type_from_lua_userdata!(BrickColor);
|
@ -1,7 +1,10 @@
|
||||
use mlua::FromLua;
|
||||
|
||||
use super::number::Number;
|
||||
use super::vector3::Vector3;
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct CFrame(pub(crate)glam::Affine3A);
|
||||
pub struct CFrame(glam::Affine3A);
|
||||
|
||||
impl CFrame{
|
||||
pub fn new(
|
||||
@ -34,14 +37,14 @@ fn vec3_from_glam(v:rbx_types::Vector3)->glam::Vec3A{
|
||||
glam::vec3a(v.x,v.y,v.z)
|
||||
}
|
||||
|
||||
impl Into<rbx_types::CFrame> for CFrame{
|
||||
fn into(self)->rbx_types::CFrame{
|
||||
impl From<CFrame> for rbx_types::CFrame{
|
||||
fn from(CFrame(cf):CFrame)->rbx_types::CFrame{
|
||||
rbx_types::CFrame::new(
|
||||
vec3_to_glam(self.0.translation),
|
||||
vec3_to_glam(cf.translation),
|
||||
rbx_types::Matrix3::new(
|
||||
vec3_to_glam(self.0.matrix3.x_axis),
|
||||
vec3_to_glam(self.0.matrix3.y_axis),
|
||||
vec3_to_glam(self.0.matrix3.z_axis),
|
||||
vec3_to_glam(cf.matrix3.x_axis),
|
||||
vec3_to_glam(cf.matrix3.y_axis),
|
||||
vec3_to_glam(cf.matrix3.z_axis),
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -60,16 +63,25 @@ impl From<rbx_types::CFrame> for CFrame{
|
||||
}
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
let cframe_table=lua.create_table()?;
|
||||
let table=lua.create_table()?;
|
||||
|
||||
//CFrame.new
|
||||
cframe_table.raw_set("new",
|
||||
lua.create_function(|_,tuple:(
|
||||
mlua::Value,mlua::Value,Option<f32>,
|
||||
Option<f32>,Option<f32>,Option<f32>,
|
||||
Option<f32>,Option<f32>,Option<f32>,
|
||||
Option<f32>,Option<f32>,Option<f32>,
|
||||
table.raw_set("new",
|
||||
lua.create_function(|lua,tuple:(
|
||||
mlua::Value,mlua::Value,Option<Number>,
|
||||
Option<Number>,Option<Number>,Option<Number>,
|
||||
Option<Number>,Option<Number>,Option<Number>,
|
||||
Option<Number>,Option<Number>,Option<Number>,
|
||||
)|match tuple{
|
||||
//CFrame.new()
|
||||
(
|
||||
mlua::Value::Nil,mlua::Value::Nil,None,
|
||||
None,None,None,
|
||||
None,None,None,
|
||||
None,None,None,
|
||||
)=>{
|
||||
Ok(CFrame(glam::Affine3A::IDENTITY))
|
||||
},
|
||||
//CFrame.new(pos)
|
||||
(
|
||||
mlua::Value::UserData(pos),mlua::Value::Nil,None,
|
||||
@ -77,8 +89,8 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
None,None,None,
|
||||
None,None,None,
|
||||
)=>{
|
||||
let pos:Vector3=pos.take()?;
|
||||
Ok(CFrame::point(pos.0.x,pos.0.y,pos.0.z))
|
||||
let Vector3(pos):&Vector3=&*pos.borrow()?;
|
||||
Ok(CFrame::point(pos.x,pos.y,pos.z))
|
||||
},
|
||||
//TODO: CFrame.new(pos,look)
|
||||
(
|
||||
@ -87,85 +99,99 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
None,None,None,
|
||||
None,None,None,
|
||||
)=>{
|
||||
let _pos:Vector3=pos.take()?;
|
||||
let _look:Vector3=look.take()?;
|
||||
let _pos:&Vector3=&*pos.borrow()?;
|
||||
let _look:&Vector3=&*look.borrow()?;
|
||||
Err(mlua::Error::runtime("Not yet implemented"))
|
||||
},
|
||||
//CFrame.new(x,y,z)
|
||||
(
|
||||
mlua::Value::Number(x),mlua::Value::Number(y),Some(z),
|
||||
x,y,Some(z),
|
||||
None,None,None,
|
||||
None,None,None,
|
||||
None,None,None,
|
||||
)=>Ok(CFrame::point(x as f32,y as f32,z)),
|
||||
)=>Ok(CFrame::point(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into())),
|
||||
//CFrame.new(x,y,z,xx,yx,zx,xy,yy,zy,xz,yz,zz)
|
||||
(
|
||||
mlua::Value::Number(x),mlua::Value::Number(y),Some(z),
|
||||
x,y,Some(z),
|
||||
Some(xx),Some(yx),Some(zx),
|
||||
Some(xy),Some(yy),Some(zy),
|
||||
Some(xz),Some(yz),Some(zz),
|
||||
)=>Ok(CFrame::new(x as f32,y as f32,z,
|
||||
xx,yx,zx,
|
||||
xy,yy,zy,
|
||||
xz,yz,zz,
|
||||
)=>Ok(CFrame::new(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into(),
|
||||
xx.into(),yx.into(),zx.into(),
|
||||
xy.into(),yy.into(),zy.into(),
|
||||
xz.into(),yz.into(),zz.into(),
|
||||
)),
|
||||
_=>Err(mlua::Error::runtime("Invalid arguments"))
|
||||
})?
|
||||
)?;
|
||||
|
||||
//CFrame.Angles
|
||||
cframe_table.raw_set("Angles",
|
||||
lua.create_function(|_,(x,y,z):(f32,f32,f32)|
|
||||
Ok(CFrame::angles(x,y,z))
|
||||
)?
|
||||
let from_euler_angles=lua.create_function(|_,(x,y,z):(Number,Number,Number)|
|
||||
Ok(CFrame::angles(x.into(),y.into(),z.into()))
|
||||
)?;
|
||||
table.raw_set("Angles",from_euler_angles.clone())?;
|
||||
table.raw_set("fromEulerAnglesXYZ",from_euler_angles.clone())?;
|
||||
table.raw_set("FromEulerAnglesXYZ",from_euler_angles)?;
|
||||
|
||||
globals.set("CFrame",cframe_table)?;
|
||||
globals.set("CFrame",table)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl mlua::UserData for CFrame{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
|
||||
//CFrame.p
|
||||
fields.add_field_method_get("p",|_,this|Ok(Vector3(this.0.translation)));
|
||||
fields.add_field_method_get("p",|_,CFrame(this)|Ok(Vector3(this.translation)));
|
||||
fields.add_field_method_get("x",|_,CFrame(this)|Ok(this.translation.x));
|
||||
fields.add_field_method_get("X",|_,CFrame(this)|Ok(this.translation.x));
|
||||
fields.add_field_method_get("y",|_,CFrame(this)|Ok(this.translation.y));
|
||||
fields.add_field_method_get("Y",|_,CFrame(this)|Ok(this.translation.y));
|
||||
fields.add_field_method_get("z",|_,CFrame(this)|Ok(this.translation.z));
|
||||
fields.add_field_method_get("Z",|_,CFrame(this)|Ok(this.translation.z));
|
||||
fields.add_field_method_get("rightVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.x_axis)));
|
||||
fields.add_field_method_get("RightVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.x_axis)));
|
||||
fields.add_field_method_get("upVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.y_axis)));
|
||||
fields.add_field_method_get("UpVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.y_axis)));
|
||||
fields.add_field_method_get("lookVector",|_,CFrame(this)|Ok(Vector3(-this.matrix3.z_axis)));
|
||||
fields.add_field_method_get("LookVector",|_,CFrame(this)|Ok(Vector3(-this.matrix3.z_axis)));
|
||||
fields.add_field_method_get("XVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(0))));
|
||||
fields.add_field_method_get("YVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(1))));
|
||||
fields.add_field_method_get("ZVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(2))));
|
||||
}
|
||||
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
|
||||
methods.add_method("components",|_,this,()|Ok((
|
||||
this.0.translation.x,
|
||||
this.0.translation.y,
|
||||
this.0.translation.z,
|
||||
this.0.matrix3.x_axis.x,
|
||||
this.0.matrix3.y_axis.x,
|
||||
this.0.matrix3.z_axis.x,
|
||||
this.0.matrix3.x_axis.y,
|
||||
this.0.matrix3.y_axis.y,
|
||||
this.0.matrix3.z_axis.y,
|
||||
this.0.matrix3.x_axis.z,
|
||||
this.0.matrix3.y_axis.z,
|
||||
this.0.matrix3.z_axis.z,
|
||||
methods.add_method("components",|_,CFrame(this),()|Ok((
|
||||
this.translation.x,
|
||||
this.translation.y,
|
||||
this.translation.z,
|
||||
this.matrix3.x_axis.x,
|
||||
this.matrix3.y_axis.x,
|
||||
this.matrix3.z_axis.x,
|
||||
this.matrix3.x_axis.y,
|
||||
this.matrix3.y_axis.y,
|
||||
this.matrix3.z_axis.y,
|
||||
this.matrix3.x_axis.z,
|
||||
this.matrix3.y_axis.z,
|
||||
this.matrix3.z_axis.z,
|
||||
)));
|
||||
methods.add_method("VectorToWorldSpace",|_,this,v:Vector3|
|
||||
Ok(Vector3(this.0.transform_vector3a(v.0)))
|
||||
methods.add_method("VectorToWorldSpace",|_,CFrame(this),Vector3(v):Vector3|
|
||||
Ok(Vector3(this.transform_vector3a(v)))
|
||||
);
|
||||
|
||||
//methods.add_meta_method(mlua::MetaMethod::Mul,|_,this,val:&Vector3|Ok(Vector3(this.0.matrix3*val.0+this.0.translation)));
|
||||
methods.add_meta_function(mlua::MetaMethod::Mul,|_,(this,val):(Self,Self)|Ok(Self(this.0*val.0)));
|
||||
methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self|
|
||||
methods.add_meta_function(mlua::MetaMethod::Mul,|_,(CFrame(this),CFrame(val)):(Self,Self)|Ok(Self(this*val)));
|
||||
methods.add_meta_function(mlua::MetaMethod::ToString,|_,CFrame(this):Self|
|
||||
Ok(format!("CFrame.new({},{},{},{},{},{},{},{},{},{},{},{})",
|
||||
this.0.translation.x,
|
||||
this.0.translation.y,
|
||||
this.0.translation.z,
|
||||
this.0.matrix3.x_axis.x,
|
||||
this.0.matrix3.y_axis.x,
|
||||
this.0.matrix3.z_axis.x,
|
||||
this.0.matrix3.x_axis.y,
|
||||
this.0.matrix3.y_axis.y,
|
||||
this.0.matrix3.z_axis.y,
|
||||
this.0.matrix3.x_axis.z,
|
||||
this.0.matrix3.y_axis.z,
|
||||
this.0.matrix3.z_axis.z,
|
||||
this.translation.x,
|
||||
this.translation.y,
|
||||
this.translation.z,
|
||||
this.matrix3.x_axis.x,
|
||||
this.matrix3.y_axis.x,
|
||||
this.matrix3.z_axis.x,
|
||||
this.matrix3.x_axis.y,
|
||||
this.matrix3.y_axis.y,
|
||||
this.matrix3.z_axis.y,
|
||||
this.matrix3.x_axis.z,
|
||||
this.matrix3.y_axis.z,
|
||||
this.matrix3.z_axis.z,
|
||||
))
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use super::number::Number;
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct Color3{
|
||||
r:f32,
|
||||
@ -8,28 +10,37 @@ impl Color3{
|
||||
pub const fn new(r:f32,g:f32,b:f32)->Self{
|
||||
Self{r,g,b}
|
||||
}
|
||||
pub const fn from_rgb(r:u8,g:u8,b:u8)->Self{
|
||||
Color3::new(r as f32/255.0,g as f32/255.0,b as f32/255.0)
|
||||
}
|
||||
}
|
||||
impl Into<rbx_types::Color3> for Color3{
|
||||
fn into(self)->rbx_types::Color3{
|
||||
rbx_types::Color3::new(self.r,self.g,self.b)
|
||||
impl From<rbx_types::Color3> for Color3{
|
||||
fn from(value:rbx_types::Color3)->Color3{
|
||||
Color3::new(value.r,value.g,value.b)
|
||||
}
|
||||
}
|
||||
impl From<Color3> for rbx_types::Color3{
|
||||
fn from(value:Color3)->rbx_types::Color3{
|
||||
rbx_types::Color3::new(value.r,value.g,value.b)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
let color3_table=lua.create_table()?;
|
||||
let table=lua.create_table()?;
|
||||
|
||||
color3_table.raw_set("new",
|
||||
lua.create_function(|_,(r,g,b):(f32,f32,f32)|
|
||||
Ok(Color3::new(r,g,b))
|
||||
)?
|
||||
)?;
|
||||
color3_table.raw_set("fromRGB",
|
||||
lua.create_function(|_,(r,g,b):(u8,u8,u8)|
|
||||
Ok(Color3::new(r as f32/255.0,g as f32/255.0,b as f32/255.0))
|
||||
table.raw_set("new",
|
||||
lua.create_function(|_,(r,g,b):(Number,Number,Number)|
|
||||
Ok(Color3::new(r.into(),g.into(),b.into()))
|
||||
)?
|
||||
)?;
|
||||
|
||||
globals.set("Color3",color3_table)?;
|
||||
let from_rgb=lua.create_function(|_,(r,g,b):(u8,u8,u8)|
|
||||
Ok(Color3::from_rgb(r,g,b))
|
||||
)?;
|
||||
table.raw_set("fromRGB",from_rgb.clone())?;
|
||||
table.raw_set("FromRGB",from_rgb)?;
|
||||
|
||||
globals.set("Color3",table)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,31 +1,29 @@
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct ColorSequence{}
|
||||
#[derive(Clone)]
|
||||
pub struct ColorSequence(rbx_types::ColorSequence);
|
||||
impl ColorSequence{
|
||||
pub const fn new()->Self{
|
||||
Self{}
|
||||
pub const fn new(keypoints:Vec<rbx_types::ColorSequenceKeypoint>)->Self{
|
||||
Self(rbx_types::ColorSequence{keypoints})
|
||||
}
|
||||
}
|
||||
impl Into<rbx_types::ColorSequence> for ColorSequence{
|
||||
fn into(self)->rbx_types::ColorSequence{
|
||||
rbx_types::ColorSequence{
|
||||
keypoints:Vec::new()
|
||||
}
|
||||
impl From<ColorSequence> for rbx_types::ColorSequence{
|
||||
fn from(ColorSequence(value):ColorSequence)->rbx_types::ColorSequence{
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
let number_sequence_table=lua.create_table()?;
|
||||
let table=lua.create_table()?;
|
||||
|
||||
number_sequence_table.raw_set("new",
|
||||
table.raw_set("new",
|
||||
lua.create_function(|_,_:mlua::MultiValue|
|
||||
Ok(ColorSequence::new())
|
||||
Ok(ColorSequence::new(Vec::new()))
|
||||
)?
|
||||
)?;
|
||||
|
||||
globals.set("ColorSequence",number_sequence_table)?;
|
||||
globals.set("ColorSequence",table)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl mlua::UserData for ColorSequence{}
|
||||
type_from_lua_userdata!(ColorSequence);
|
||||
type_from_lua_userdata_clone!(ColorSequence);
|
||||
|
@ -1,63 +1,136 @@
|
||||
use mlua::IntoLua;
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct Enum(u32);
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct EnumItems;
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct EnumItem<'a>{
|
||||
ed:&'a rbx_reflection::EnumDescriptor<'a>,
|
||||
name:Option<&'a str>,
|
||||
value:u32,
|
||||
}
|
||||
|
||||
impl Into<rbx_types::Enum> for Enum{
|
||||
fn into(self)->rbx_types::Enum{
|
||||
rbx_types::Enum::from_u32(self.0)
|
||||
impl<'a> EnumItem<'a>{
|
||||
fn known_name((name,&value):(&'a std::borrow::Cow<'a,str>,&u32))->Self{
|
||||
Self{name:Some(name.as_ref()),value}
|
||||
}
|
||||
}
|
||||
impl<'a> From<rbx_types::Enum> for EnumItem<'a>{
|
||||
fn from(e:rbx_types::Enum)->Self{
|
||||
EnumItem{
|
||||
name:None,
|
||||
value:e.to_u32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<EnumItem<'_>> for rbx_types::Enum{
|
||||
fn from(e:EnumItem)->rbx_types::Enum{
|
||||
rbx_types::Enum::from_u32(e.value)
|
||||
}
|
||||
}
|
||||
impl PartialEq for EnumItem<'_>{
|
||||
fn eq(&self,other:&EnumItem<'_>)->bool{
|
||||
self.value==other.value&&{
|
||||
// if both names are known, they must match, otherwise whatever
|
||||
match (self.name,other.name){
|
||||
(Some(lhs),Some(rhs))=>lhs==rhs,
|
||||
_=>true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EnumItem<'a>{
|
||||
const fn new(ed:&'a rbx_reflection::EnumDescriptor)->Self{
|
||||
Self{ed}
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct Enums;
|
||||
impl Enums{
|
||||
pub fn get(&self,index:&str)->Option<EnumItems<'static>>{
|
||||
let db=rbx_reflection_database::get();
|
||||
db.enums.get(index).map(|ed|EnumItems{ed})
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct EnumItems<'a>{
|
||||
ed:&'a rbx_reflection::EnumDescriptor<'a>,
|
||||
}
|
||||
|
||||
impl<'a> EnumItems<'a>{
|
||||
pub fn from_value(&self,value:u32)->Option<EnumItem<'a>>{
|
||||
self.ed.items.iter().find(|&(_,&v)|v==value).map(EnumItem::known_name)
|
||||
}
|
||||
pub fn from_name(&self,name:&str)->Option<EnumItem<'a>>{
|
||||
self.ed.items.get_key_value(name).map(EnumItem::known_name)
|
||||
}
|
||||
pub fn from_enum(&self,enum_item:EnumItem)->Option<EnumItem<'a>>{
|
||||
match enum_item.name{
|
||||
Some(s)=>{
|
||||
let got=self.from_name(s)?;
|
||||
(got.value==enum_item.value).then_some(got)
|
||||
},
|
||||
None=>self.from_value(enum_item.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CoerceEnum<'a>{
|
||||
Integer(i32),
|
||||
String(mlua::String),
|
||||
Enum(EnumItem<'a>),
|
||||
}
|
||||
impl CoerceEnum<'_>{
|
||||
pub fn coerce_to<'a>(self,enum_items:EnumItems<'a>)->mlua::Result<EnumItem<'a>>{
|
||||
match self{
|
||||
CoerceEnum::Integer(int)=>enum_items.from_value(int as u32),
|
||||
CoerceEnum::String(s)=>enum_items.from_name(&*s.to_str()?),
|
||||
CoerceEnum::Enum(enum_item)=>enum_items.from_enum(enum_item),
|
||||
}.ok_or_else(||mlua::Error::runtime(format!("Bad {} EnumItem",enum_items.ed.name)))
|
||||
}
|
||||
}
|
||||
|
||||
impl mlua::FromLua for CoerceEnum<'_>{
|
||||
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
|
||||
match value{
|
||||
mlua::Value::Integer(int)=>Ok(CoerceEnum::Integer(int)),
|
||||
mlua::Value::String(s)=>Ok(CoerceEnum::String(s)),
|
||||
mlua::Value::UserData(ud)=>Ok(CoerceEnum::Enum(*ud.borrow()?)),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(Enum),other))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_globals(_lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
globals.set("Enum",EnumItems)
|
||||
globals.set("Enum",Enums)
|
||||
}
|
||||
|
||||
impl mlua::UserData for EnumItem<'_>{
|
||||
impl mlua::UserData for EnumItems<'static>{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
|
||||
}
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
|
||||
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,val):(EnumItem<'_>,mlua::String)|{
|
||||
match this.ed.items.get(&*val.to_str()?){
|
||||
Some(&id)=>Enum(id).into_lua(lua),
|
||||
None=>mlua::Value::Nil.into_lua(lua),
|
||||
}
|
||||
methods.add_method("FromName",|_,this:&EnumItems,name:mlua::String|Ok(this.from_name(&*name.to_str()?)));
|
||||
methods.add_method("FromValue",|_,this:&EnumItems,value:u32|Ok(this.from_value(value)));
|
||||
methods.add_method("GetEnumItems",|_,this:&EnumItems,()|->mlua::Result<Vec<EnumItem>>{
|
||||
Ok(this.ed.items.iter().map(EnumItem::known_name).collect())
|
||||
});
|
||||
methods.add_meta_function(mlua::MetaMethod::Index,|_,(this,val):(EnumItems,mlua::String)|{
|
||||
let index=&*val.to_str()?;
|
||||
Ok(this.ed.items.get_key_value(index).map(EnumItem::known_name))
|
||||
});
|
||||
}
|
||||
}
|
||||
type_from_lua_userdata_lua_lifetime!(EnumItems);
|
||||
|
||||
impl mlua::UserData for Enums{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
|
||||
}
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
|
||||
methods.add_meta_function(mlua::MetaMethod::Index,|_,(enums,val):(Self,mlua::String)|{
|
||||
Ok(enums.get(&*val.to_str()?))
|
||||
});
|
||||
}
|
||||
}
|
||||
type_from_lua_userdata!(Enums);
|
||||
|
||||
impl mlua::UserData for EnumItem<'_>{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
|
||||
fields.add_field_method_get("Name",|_,this|Ok(this.name));
|
||||
fields.add_field_method_get("Value",|_,this|Ok(this.value));
|
||||
}
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
|
||||
methods.add_meta_function(mlua::MetaMethod::Eq,|_,(lhs,rhs):(EnumItem<'_>,EnumItem<'_>)|{
|
||||
Ok(lhs==rhs)
|
||||
});
|
||||
}
|
||||
}
|
||||
type_from_lua_userdata_lua_lifetime!(EnumItem);
|
||||
|
||||
impl mlua::UserData for EnumItems{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
|
||||
}
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
|
||||
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(_,val):(Self,mlua::String)|{
|
||||
let db=rbx_reflection_database::get();
|
||||
match db.enums.get(&*val.to_str()?){
|
||||
Some(ed)=>EnumItem::new(ed).into_lua(lua),
|
||||
None=>mlua::Value::Nil.into_lua(lua),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
type_from_lua_userdata!(EnumItems);
|
||||
|
||||
impl mlua::UserData for Enum{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
|
||||
}
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(_methods:&mut M){
|
||||
}
|
||||
}
|
||||
type_from_lua_userdata!(Enum);
|
||||
|
@ -2,48 +2,47 @@ use std::collections::{hash_map::Entry,HashMap};
|
||||
|
||||
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
|
||||
use rbx_types::Ref;
|
||||
use rbx_dom_weak::{InstanceBuilder,WeakDom};
|
||||
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
|
||||
|
||||
use crate::util::static_ustr;
|
||||
use crate::runner::vector3::Vector3;
|
||||
use crate::runner::number::Number;
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
//class functions store
|
||||
lua.set_app_data(ClassMethodsStore::default());
|
||||
lua.set_app_data(InstanceValueStore::default());
|
||||
|
||||
let instance_table=lua.create_table()?;
|
||||
let table=lua.create_table()?;
|
||||
|
||||
//Instance.new
|
||||
instance_table.raw_set("new",
|
||||
table.raw_set("new",
|
||||
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
|
||||
let class_name_str=&*class_name.to_str()?;
|
||||
let parent=parent.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
|
||||
let parent_ref=parent.map_or(Ref::none(),|instance|instance.referent);
|
||||
dom_mut(lua,|dom|{
|
||||
//TODO: Nil instances
|
||||
Ok(Instance::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str))))
|
||||
Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str))))
|
||||
})
|
||||
})?
|
||||
)?;
|
||||
|
||||
globals.set("Instance",instance_table)?;
|
||||
globals.set("Instance",table)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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"))?;
|
||||
let mut dom=lua.app_data_mut::<crate::context::LuaAppData>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
|
||||
f(*dom)
|
||||
}
|
||||
|
||||
fn coerce_float32(value:&mlua::Value)->Option<f32>{
|
||||
match value{
|
||||
&mlua::Value::Integer(i)=>Some(i as f32),
|
||||
&mlua::Value::Number(f)=>Some(f as f32),
|
||||
_=>None,
|
||||
}
|
||||
pub fn class_is_a(class:&str,superclass:&str)->bool{
|
||||
let db=rbx_reflection_database::get();
|
||||
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
|
||||
return false;
|
||||
};
|
||||
db.has_superclass(class,superclass)
|
||||
}
|
||||
|
||||
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
|
||||
let mut full_name=instance.name.clone();
|
||||
let mut pref=instance.parent();
|
||||
@ -58,7 +57,7 @@ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->S
|
||||
pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=script.get(dom)?;
|
||||
let source=match instance.properties.get("Source"){
|
||||
let source=match instance.properties.get(&static_ustr("Source")){
|
||||
Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
|
||||
_=>Err(mlua::Error::external("Missing script.Source"))?,
|
||||
};
|
||||
@ -80,14 +79,32 @@ pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance
|
||||
dom.descendants_of(instance.referent()).find(|&inst|inst.class==class)
|
||||
}
|
||||
|
||||
pub fn find_first_child_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
let db=rbx_reflection_database::get();
|
||||
let superclass_descriptor=db.classes.get(superclass)?;
|
||||
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
|
||||
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
|
||||
})
|
||||
}
|
||||
pub fn find_first_descendant_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
let db=rbx_reflection_database::get();
|
||||
let superclass_descriptor=db.classes.get(superclass)?;
|
||||
dom.descendants_of(instance.referent()).find(|inst|{
|
||||
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct Instance{
|
||||
referent:Ref,
|
||||
}
|
||||
impl Instance{
|
||||
pub const fn new(referent:Ref)->Self{
|
||||
pub const fn new_unchecked(referent:Ref)->Self{
|
||||
Self{referent}
|
||||
}
|
||||
pub fn new(referent:Ref)->Option<Self>{
|
||||
referent.is_some().then_some(Self{referent})
|
||||
}
|
||||
pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a rbx_dom_weak::Instance>{
|
||||
dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
|
||||
}
|
||||
@ -97,40 +114,25 @@ impl Instance{
|
||||
}
|
||||
type_from_lua_userdata!(Instance);
|
||||
|
||||
//TODO: update rbx_reflection and use dom.superclasses_iter
|
||||
pub struct SuperClassIter<'a> {
|
||||
database: &'a rbx_reflection::ReflectionDatabase<'a>,
|
||||
descriptor: Option<&'a rbx_reflection::ClassDescriptor<'a>>,
|
||||
}
|
||||
impl<'a> SuperClassIter<'a> {
|
||||
fn next_descriptor(&self) -> Option<&'a rbx_reflection::ClassDescriptor<'a>> {
|
||||
let superclass = self.descriptor?.superclass.as_ref()?;
|
||||
self.database.classes.get(superclass)
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for SuperClassIter<'a> {
|
||||
type Item = &'a rbx_reflection::ClassDescriptor<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let next_descriptor = self.next_descriptor();
|
||||
std::mem::replace(&mut self.descriptor, next_descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
impl mlua::UserData for Instance{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
|
||||
fields.add_field_method_get("Parent",|lua,this|{
|
||||
fn get_parent(lua:&mlua::Lua,this:&Instance)->mlua::Result<Option<Instance>>{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
Ok(Instance::new(instance.parent()))
|
||||
})
|
||||
});
|
||||
fields.add_field_method_set("Parent",|lua,this,val:Option<Instance>|{
|
||||
let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
|
||||
}
|
||||
fields.add_field_method_get("parent",get_parent);
|
||||
fields.add_field_method_get("Parent",get_parent);
|
||||
fn set_parent(lua:&mlua::Lua,this:&mut Instance,new_parent:Option<Instance>)->mlua::Result<()>{
|
||||
let parent_ref=new_parent.map_or(Ref::none(),|instance|instance.referent);
|
||||
dom_mut(lua,|dom|{
|
||||
dom.transfer_within(this.referent,parent.referent);
|
||||
dom.transfer_within(this.referent,parent_ref);
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
fields.add_field_method_set("parent",set_parent);
|
||||
fields.add_field_method_set("Parent",set_parent);
|
||||
fields.add_field_method_get("Name",|lua,this|{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
@ -148,22 +150,38 @@ impl mlua::UserData for Instance{
|
||||
fields.add_field_method_get("ClassName",|lua,this|{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
Ok(instance.class.clone())
|
||||
Ok(instance.class.to_owned())
|
||||
})
|
||||
});
|
||||
}
|
||||
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
|
||||
methods.add_method("GetChildren",|lua,this,_:()|
|
||||
fn clone(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Instance>{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance_ref=dom.clone_within(this.referent);
|
||||
Ok(Instance::new_unchecked(instance_ref))
|
||||
})
|
||||
}
|
||||
methods.add_method("clone",clone);
|
||||
methods.add_method("Clone",clone);
|
||||
fn get_children(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Vec<Instance>>{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
let children:Vec<_>=instance
|
||||
.children()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Instance::new)
|
||||
.map(Instance::new_unchecked)
|
||||
.collect();
|
||||
Ok(children)
|
||||
})
|
||||
}
|
||||
methods.add_method("children",get_children);
|
||||
methods.add_method("GetChildren",get_children);
|
||||
methods.add_method("GetFullName",|lua,this,()|
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
Ok(get_full_name(dom,instance))
|
||||
})
|
||||
);
|
||||
fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
|
||||
let name_str=&*name.to_str()?;
|
||||
@ -175,7 +193,7 @@ impl mlua::UserData for Instance{
|
||||
false=>find_first_child(dom,instance,name_str),
|
||||
}
|
||||
.map(|instance|
|
||||
Instance::new(instance.referent())
|
||||
Instance::new_unchecked(instance.referent())
|
||||
)
|
||||
)
|
||||
})
|
||||
@ -185,13 +203,29 @@ impl mlua::UserData for Instance{
|
||||
methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
|
||||
let class_str=&*class.to_str()?;
|
||||
dom_mut(lua,|dom|{
|
||||
let inst=this.get(dom)?;
|
||||
Ok(
|
||||
match search_descendants.unwrap_or(false){
|
||||
true=>find_first_descendant_of_class(dom,this.get(dom)?,class_str),
|
||||
false=>find_first_child_of_class(dom,this.get(dom)?,class_str),
|
||||
true=>find_first_descendant_of_class(dom,inst,class_str),
|
||||
false=>find_first_child_of_class(dom,inst,class_str),
|
||||
}
|
||||
.map(|instance|
|
||||
Instance::new(instance.referent())
|
||||
Instance::new_unchecked(instance.referent())
|
||||
)
|
||||
)
|
||||
})
|
||||
});
|
||||
methods.add_method("FindFirstChildWhichIsA",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
|
||||
let class_str=&*class.to_str()?;
|
||||
dom_mut(lua,|dom|{
|
||||
let inst=this.get(dom)?;
|
||||
Ok(
|
||||
match search_descendants.unwrap_or(false){
|
||||
true=>find_first_descendant_which_is_a(dom,inst,class_str),
|
||||
false=>find_first_child_which_is_a(dom,inst,class_str),
|
||||
}
|
||||
.map(|instance|
|
||||
Instance::new_unchecked(instance.referent())
|
||||
)
|
||||
)
|
||||
})
|
||||
@ -201,24 +235,42 @@ impl mlua::UserData for Instance{
|
||||
let children:Vec<_>=dom
|
||||
.descendants_of(this.referent)
|
||||
.map(|instance|
|
||||
Instance::new(instance.referent())
|
||||
Instance::new_unchecked(instance.referent())
|
||||
)
|
||||
.collect();
|
||||
Ok(children)
|
||||
})
|
||||
);
|
||||
methods.add_method("IsA",|lua,this,classname:mlua::String|
|
||||
methods.add_method("IsAncestorOf",|lua,this,descendant:Instance|
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=descendant.get(dom)?;
|
||||
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==this.referent))
|
||||
})
|
||||
);
|
||||
methods.add_method("IsDescendantOf",|lua,this,ancestor:Instance|
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?))
|
||||
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent))
|
||||
})
|
||||
);
|
||||
methods.add_method("Destroy",|lua,this,()|
|
||||
fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{
|
||||
dom_mut(lua,|dom|{
|
||||
dom.destroy(this.referent);
|
||||
let instance=this.get(dom)?;
|
||||
Ok(class_is_a(instance.class.as_str(),&*classname.to_str()?))
|
||||
})
|
||||
}
|
||||
methods.add_method("isA",is_a);
|
||||
methods.add_method("IsA",is_a);
|
||||
fn destroy(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<()>{
|
||||
dom_mut(lua,|dom|{
|
||||
dom.transfer_within(this.referent,Ref::none());
|
||||
Ok(())
|
||||
})
|
||||
);
|
||||
}
|
||||
methods.add_method("remove",destroy);
|
||||
methods.add_method("Remove",destroy);
|
||||
methods.add_method("destroy",destroy);
|
||||
methods.add_method("Destroy",destroy);
|
||||
methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
@ -232,39 +284,38 @@ impl mlua::UserData for Instance{
|
||||
//println!("__index t={} i={index:?}",instance.name);
|
||||
let db=rbx_reflection_database::get();
|
||||
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
|
||||
//Find existing property
|
||||
match instance.properties.get(index_str)
|
||||
.cloned()
|
||||
// Find existing property
|
||||
// Interestingly, ustr can know ahead of time if
|
||||
// a property does not exist in any runtime instance
|
||||
match Ustr::from_existing(index_str)
|
||||
.and_then(|index_ustr|
|
||||
instance.properties.get(&index_ustr).cloned()
|
||||
)
|
||||
//Find default value
|
||||
.or_else(||db.find_default_property(class,index_str).cloned())
|
||||
//Find virtual property
|
||||
.or_else(||{
|
||||
SuperClassIter{
|
||||
database:db,
|
||||
descriptor:Some(class),
|
||||
}
|
||||
.find_map(|class|
|
||||
find_virtual_property(&instance.properties,class,index_str)
|
||||
)
|
||||
})
|
||||
.or_else(||db.superclasses_iter(class).find_map(|class|
|
||||
find_virtual_property(&instance.properties,class,index_str)
|
||||
))
|
||||
{
|
||||
Some(rbx_types::Variant::Bool(val))=>return val.into_lua(lua),
|
||||
Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua),
|
||||
Some(rbx_types::Variant::Int64(val))=>return val.into_lua(lua),
|
||||
Some(rbx_types::Variant::Float32(val))=>return val.into_lua(lua),
|
||||
Some(rbx_types::Variant::Float64(val))=>return val.into_lua(lua),
|
||||
Some(rbx_types::Variant::Ref(val))=>return Instance::new(val).into_lua(lua),
|
||||
Some(rbx_types::Variant::CFrame(cf))=>return Into::<crate::runner::cframe::CFrame>::into(cf).into_lua(lua),
|
||||
Some(rbx_types::Variant::Vector3(v))=>return Into::<crate::runner::vector3::Vector3>::into(v).into_lua(lua),
|
||||
Some(rbx_types::Variant::String(val))=>return val.into_lua(lua),
|
||||
Some(rbx_types::Variant::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua),
|
||||
Some(rbx_types::Variant::Enum(e))=>return crate::runner::r#enum::EnumItem::from(e).into_lua(lua),
|
||||
Some(rbx_types::Variant::Color3(c))=>return crate::runner::color3::Color3::from(c).into_lua(lua),
|
||||
Some(rbx_types::Variant::CFrame(cf))=>return crate::runner::cframe::CFrame::from(cf).into_lua(lua),
|
||||
Some(rbx_types::Variant::Vector2(v))=>return crate::runner::vector2::Vector2::from(v).into_lua(lua),
|
||||
Some(rbx_types::Variant::Vector3(v))=>return crate::runner::vector3::Vector3::from(v).into_lua(lua),
|
||||
None=>(),
|
||||
other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))),
|
||||
}
|
||||
//find a function with a matching name
|
||||
if let Some(function)=class_methods_store_mut(lua,|cf|{
|
||||
let mut iter=SuperClassIter{
|
||||
database:db,
|
||||
descriptor:Some(class),
|
||||
};
|
||||
iter.find_map(|class|{
|
||||
db.superclasses_iter(class).find_map(|class|{
|
||||
let mut class_methods=cf.get_or_create_class_methods(&class.name)?;
|
||||
class_methods.get_or_create_function(lua,index_str)
|
||||
.transpose()
|
||||
@ -274,80 +325,114 @@ impl mlua::UserData for Instance{
|
||||
}
|
||||
|
||||
//find or create an associated userdata object
|
||||
if let Some(value)=instance_value_store_mut(lua,|ivs|{
|
||||
//TODO: walk class tree somehow
|
||||
match ivs.get_or_create_instance_values(&instance){
|
||||
Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
|
||||
None=>Ok(None)
|
||||
}
|
||||
})?{
|
||||
let instance=this.get_mut(dom)?;
|
||||
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{
|
||||
return value.into_lua(lua);
|
||||
}
|
||||
// drop mutable borrow
|
||||
//find a child with a matching name
|
||||
let instance=this.get(dom)?;
|
||||
find_first_child(dom,instance,index_str)
|
||||
.map(|instance|Instance::new(instance.referent()))
|
||||
.map(|instance|Instance::new_unchecked(instance.referent()))
|
||||
.into_lua(lua)
|
||||
})
|
||||
});
|
||||
methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{
|
||||
let index_str=&*index.to_str()?;
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get_mut(dom)?;
|
||||
//println!("__newindex t={} i={index:?} v={value:?}",instance.name);
|
||||
let index_str=&*index.to_str()?;
|
||||
let db=rbx_reflection_database::get();
|
||||
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
|
||||
let mut iter=SuperClassIter{
|
||||
database:db,
|
||||
descriptor:Some(class),
|
||||
};
|
||||
let property=iter.find_map(|cls|cls.properties.get(index_str)).ok_or_else(||mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)))?;
|
||||
match &property.data_type{
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
|
||||
let typed_value:Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Userdata"))?.borrow()?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Vector3(typed_value.into()));
|
||||
},
|
||||
let property=db.superclasses_iter(class).find_map(|cls|
|
||||
cls.properties.get(index_str)
|
||||
).ok_or_else(||
|
||||
mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name))
|
||||
)?;
|
||||
let value=match &property.data_type{
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{
|
||||
let typed_value:f32=coerce_float32(&value).ok_or_else(||mlua::Error::runtime("Expected f32"))?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Float32(typed_value));
|
||||
let typed_value=Number::from_lua(value.clone(),lua)?.to_f32();
|
||||
rbx_types::Variant::Float32(typed_value)
|
||||
},
|
||||
rbx_reflection::DataType::Enum(enum_name)=>{
|
||||
let typed_value=match &value{
|
||||
&mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)),
|
||||
&mlua::Value::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)),
|
||||
mlua::Value::String(s)=>{
|
||||
let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?;
|
||||
let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?;
|
||||
Ok(rbx_types::Enum::from_u32(*e.items.get(&*s.to_str()?).ok_or_else(||mlua::Error::runtime("Invalid enum item"))?))
|
||||
},
|
||||
mlua::Value::UserData(any_user_data)=>{
|
||||
let e:crate::runner::r#enum::Enum=*any_user_data.borrow()?;
|
||||
let e:crate::runner::r#enum::EnumItem=*any_user_data.borrow()?;
|
||||
Ok(e.into())
|
||||
},
|
||||
_=>Err(mlua::Error::runtime("Expected Enum")),
|
||||
}?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Enum(typed_value));
|
||||
rbx_types::Variant::Enum(typed_value)
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{
|
||||
let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Color3(typed_value.into()));
|
||||
rbx_types::Variant::Color3(typed_value.into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{
|
||||
let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Bool(typed_value));
|
||||
rbx_types::Variant::Bool(typed_value)
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Int32)=>{
|
||||
let typed_value=value.as_i32().ok_or_else(||mlua::Error::runtime("Expected Int32"))?;
|
||||
rbx_types::Variant::Int32(typed_value)
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{
|
||||
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::String(typed_value.to_owned()));
|
||||
let typed_value=match &value{
|
||||
mlua::Value::Integer(i)=>i.to_string(),
|
||||
mlua::Value::Number(n)=>n.to_string(),
|
||||
mlua::Value::String(s)=>s.to_str()?.to_owned(),
|
||||
_=>return Err(mlua::Error::runtime("Expected string")),
|
||||
};
|
||||
rbx_types::Variant::String(typed_value)
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::UDim2)=>{
|
||||
let typed_value:&crate::runner::udim2::UDim2=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected UDim2"))?.borrow()?;
|
||||
rbx_types::Variant::UDim2(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberRange)=>{
|
||||
let typed_value:&crate::runner::number_range::NumberRange=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberRange"))?.borrow()?;
|
||||
rbx_types::Variant::NumberRange(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberSequence)=>{
|
||||
let typed_value:crate::runner::number_sequence::NumberSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::NumberSequence(typed_value.into()));
|
||||
let typed_value:&crate::runner::number_sequence::NumberSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
|
||||
rbx_types::Variant::NumberSequence(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{
|
||||
let typed_value:crate::runner::color_sequence::ColorSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
|
||||
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::ColorSequence(typed_value.into()));
|
||||
let typed_value:&crate::runner::color_sequence::ColorSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
|
||||
rbx_types::Variant::ColorSequence(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector2)=>{
|
||||
let typed_value:crate::runner::vector2::Vector2=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector2"))?.borrow()?;
|
||||
rbx_types::Variant::Vector2(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
|
||||
let typed_value:crate::runner::vector3::Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector3"))?.borrow()?;
|
||||
rbx_types::Variant::Vector3(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::CFrame)=>{
|
||||
let typed_value:crate::runner::cframe::CFrame=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected CFrame"))?.borrow()?;
|
||||
rbx_types::Variant::CFrame(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
|
||||
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_owned();
|
||||
rbx_types::Variant::ContentId(typed_value.into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
|
||||
// why clone?
|
||||
let typed_value=Option::<Instance>::from_lua(value.clone(),lua)?;
|
||||
rbx_types::Variant::Ref(typed_value.map_or(Ref::none(),|instance|instance.referent))
|
||||
},
|
||||
other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))),
|
||||
}
|
||||
};
|
||||
// the index is known to be a real property at this point
|
||||
// allow creating a permanent ustr (memory leak)
|
||||
let index_ustr=rbx_dom_weak::ustr(index_str);
|
||||
instance.properties.insert(index_ustr,value);
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
@ -372,28 +457,53 @@ type CFD=phf::Map<&'static str,// Class name
|
||||
ClassFunctionPointer
|
||||
>
|
||||
>;
|
||||
const GET_SERVICE:ClassFunctionPointer=cf!(|lua,_this,service:mlua::String|{
|
||||
dom_mut(lua,|dom|{
|
||||
//dom.root_ref()==this.referent ?
|
||||
let service=&*service.to_str()?;
|
||||
match service{
|
||||
"Lighting"|"RunService"|"Players"|"Workspace"|"MaterialService"|"TweenService"=>{
|
||||
let referent=find_first_child_of_class(dom,dom.root(),service)
|
||||
.map(|instance|instance.referent())
|
||||
.unwrap_or_else(||
|
||||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
|
||||
);
|
||||
Ok(Instance::new_unchecked(referent))
|
||||
},
|
||||
other=>Err(mlua::Error::runtime(format!("Service '{other}' not supported"))),
|
||||
}
|
||||
})
|
||||
});
|
||||
const GET_PLAYERS:ClassFunctionPointer=cf!(|_lua,_this,()|->mlua::Result<_>{
|
||||
Ok(Vec::<Instance>::new())
|
||||
});
|
||||
const NO_OP:ClassFunctionPointer=cf!(|_lua,_this,_:mlua::MultiValue|->mlua::Result<_>{Ok(())});
|
||||
static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
|
||||
"DataModel"=>phf::phf_map!{
|
||||
"GetService"=>cf!(|lua,_this,service:mlua::String|{
|
||||
dom_mut(lua,|dom|{
|
||||
//dom.root_ref()==this.referent ?
|
||||
let service=&*service.to_str()?;
|
||||
match service{
|
||||
"Lighting"|"RunService"=>{
|
||||
let referent=find_first_child_of_class(dom,dom.root(),service)
|
||||
.map(|instance|instance.referent())
|
||||
.unwrap_or_else(||
|
||||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
|
||||
);
|
||||
Ok(Instance::new(referent))
|
||||
},
|
||||
other=>Err::<Instance,_>(mlua::Error::runtime(format!("Service '{other}' not supported"))),
|
||||
}
|
||||
})
|
||||
}),
|
||||
"service"=>GET_SERVICE,
|
||||
"GetService"=>GET_SERVICE,
|
||||
},
|
||||
"Terrain"=>phf::phf_map!{
|
||||
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::Enum)|mlua::Result::Ok(()))
|
||||
"FillBall"=>cf!(|_lua,_,_:(Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
|
||||
},
|
||||
"Players"=>phf::phf_map!{
|
||||
"players"=>GET_PLAYERS,
|
||||
"GetPlayers"=>GET_PLAYERS,
|
||||
},
|
||||
"Sound"=>phf::phf_map!{
|
||||
"Play"=>NO_OP,
|
||||
},
|
||||
"TweenService"=>phf::phf_map!{
|
||||
"Create"=>cf!(|_lua,_,(instance,tween_info,goal):(Instance,crate::runner::tween_info::TweenInfo,mlua::Table)|->mlua::Result<_>{
|
||||
Ok(crate::runner::tween::Tween::create(
|
||||
instance,
|
||||
tween_info,
|
||||
goal,
|
||||
))
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@ -480,78 +590,70 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
|
||||
};
|
||||
|
||||
fn find_virtual_property(
|
||||
properties:&HashMap<String,rbx_types::Variant>,
|
||||
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
|
||||
class:&rbx_reflection::ClassDescriptor,
|
||||
index:&str
|
||||
index:&str,
|
||||
)->Option<rbx_types::Variant>{
|
||||
//Find virtual property
|
||||
let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?;
|
||||
let virtual_property=class_virtual_properties.get(index)?;
|
||||
|
||||
//Get source property
|
||||
let variant=properties.get(virtual_property.property)?;
|
||||
let variant=properties.get(&static_ustr(virtual_property.property))?;
|
||||
|
||||
//Transform Source property with provided function
|
||||
(virtual_property.pointer)(variant)
|
||||
}
|
||||
|
||||
// lazy-loaded per-instance userdata values
|
||||
// This whole thing is a bad idea and a garbage collection nightmare.
|
||||
// TODO: recreate rbx_dom_weak with my own instance type that owns this data.
|
||||
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
|
||||
type LUD=phf::Map<&'static str,// Class name
|
||||
phf::Map<&'static str,// Value name
|
||||
CreateUserData
|
||||
>
|
||||
>;
|
||||
fn create_script_signal(lua:&mlua::Lua)->mlua::Result<mlua::AnyUserData>{
|
||||
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
|
||||
}
|
||||
static LAZY_USER_DATA:LUD=phf::phf_map!{
|
||||
"RunService"=>phf::phf_map!{
|
||||
"RenderStepped"=>|lua|{
|
||||
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
|
||||
},
|
||||
"Stepped"=>create_script_signal,
|
||||
"Heartbeat"=>create_script_signal,
|
||||
"RenderStepped"=>create_script_signal,
|
||||
},
|
||||
"Players"=>phf::phf_map!{
|
||||
"PlayerAdded"=>create_script_signal,
|
||||
},
|
||||
"BasePart"=>phf::phf_map!{
|
||||
"Touched"=>create_script_signal,
|
||||
"TouchEnded"=>create_script_signal,
|
||||
},
|
||||
"Instance"=>phf::phf_map!{
|
||||
"ChildAdded"=>create_script_signal,
|
||||
"ChildRemoved"=>create_script_signal,
|
||||
"DescendantAdded"=>create_script_signal,
|
||||
"DescendantRemoved"=>create_script_signal,
|
||||
},
|
||||
"ClickDetector"=>phf::phf_map!{
|
||||
"MouseClick"=>create_script_signal,
|
||||
},
|
||||
};
|
||||
#[derive(Default)]
|
||||
pub struct InstanceValueStore{
|
||||
values:HashMap<Ref,
|
||||
HashMap<&'static str,
|
||||
mlua::AnyUserData
|
||||
>
|
||||
>,
|
||||
}
|
||||
pub struct InstanceValues<'a>{
|
||||
named_values:&'static phf::Map<&'static str,CreateUserData>,
|
||||
values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
|
||||
}
|
||||
impl InstanceValueStore{
|
||||
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues>{
|
||||
LAZY_USER_DATA.get(instance.class.as_str())
|
||||
.map(|named_values|
|
||||
InstanceValues{
|
||||
named_values,
|
||||
values:self.values.entry(instance.referent())
|
||||
.or_insert_with(||HashMap::new()),
|
||||
}
|
||||
)
|
||||
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
|
||||
use std::collections::hash_map::Entry;
|
||||
let db=rbx_reflection_database::get();
|
||||
let Some(class)=db.classes.get(instance.class.as_str())else{
|
||||
return Ok(None)
|
||||
};
|
||||
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass|
|
||||
// find pair (class,index)
|
||||
LAZY_USER_DATA.get(&superclass.name)
|
||||
.and_then(|map|map.get_entry(index))
|
||||
){
|
||||
let index_ustr=static_ustr(static_str);
|
||||
return Ok(Some(match instance.userdata.entry(index_ustr){
|
||||
Entry::Occupied(entry)=>entry.get().clone(),
|
||||
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
impl InstanceValues<'_>{
|
||||
pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
|
||||
Ok(match self.named_values.get_entry(index){
|
||||
Some((&static_index_str,&function_pointer))=>Some(
|
||||
match self.values.entry(static_index_str){
|
||||
Entry::Occupied(entry)=>entry.get().clone(),
|
||||
Entry::Vacant(entry)=>entry.insert(
|
||||
function_pointer(lua)?
|
||||
).clone(),
|
||||
}
|
||||
),
|
||||
None=>None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance_value_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result<T>)->mlua::Result<T>{
|
||||
let mut cf=lua.app_data_mut::<InstanceValueStore>().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?;
|
||||
f(&mut *cf)
|
||||
Ok(None)
|
||||
}
|
||||
|
@ -10,6 +10,18 @@ macro_rules! type_from_lua_userdata{
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! type_from_lua_userdata_clone{
|
||||
($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>()?.clone()),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! type_from_lua_userdata_lua_lifetime{
|
||||
($ty:ident)=>{
|
||||
impl mlua::FromLua for $ty<'static>{
|
||||
|
@ -3,10 +3,19 @@ mod macros;
|
||||
mod runner;
|
||||
|
||||
mod r#enum;
|
||||
mod task;
|
||||
mod udim;
|
||||
mod tween;
|
||||
mod udim2;
|
||||
mod color3;
|
||||
mod cframe;
|
||||
mod number;
|
||||
mod vector2;
|
||||
mod vector3;
|
||||
mod brickcolor;
|
||||
mod tween_info;
|
||||
pub mod instance;
|
||||
mod number_range;
|
||||
mod script_signal;
|
||||
mod color_sequence;
|
||||
mod number_sequence;
|
||||
|
43
lib/roblox_emulator/src/runner/number.rs
Normal file
43
lib/roblox_emulator/src/runner/number.rs
Normal file
@ -0,0 +1,43 @@
|
||||
// the goal of this module is to provide an intermediate type
|
||||
// that is guaranteed to be some kind of number, and provide
|
||||
// methods to coerce it into various more specific types.
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub enum Number{
|
||||
Integer(i32),
|
||||
Number(f64),
|
||||
}
|
||||
macro_rules! impl_ty{
|
||||
($ident:ident,$ty:ty)=>{
|
||||
impl Number{
|
||||
#[inline]
|
||||
pub fn $ident(self)->$ty{
|
||||
match self{
|
||||
Self::Integer(int)=>int as $ty,
|
||||
Self::Number(num)=>num as $ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Number> for $ty{
|
||||
fn from(value:Number)->$ty{
|
||||
value.$ident()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_ty!(to_u32,u32);
|
||||
impl_ty!(to_i32,i32);
|
||||
impl_ty!(to_f32,f32);
|
||||
impl_ty!(to_u64,u64);
|
||||
impl_ty!(to_i64,i64);
|
||||
impl_ty!(to_f64,f64);
|
||||
|
||||
impl mlua::FromLua for Number{
|
||||
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
|
||||
match value{
|
||||
mlua::Value::Integer(int)=>Ok(Number::Integer(int)),
|
||||
mlua::Value::Number(num)=>Ok(Number::Number(num)),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(Number),other))),
|
||||
}
|
||||
}
|
||||
}
|
34
lib/roblox_emulator/src/runner/number_range.rs
Normal file
34
lib/roblox_emulator/src/runner/number_range.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use super::number::Number;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NumberRange(rbx_types::NumberRange);
|
||||
impl NumberRange{
|
||||
pub const fn new(min:f32,max:f32)->Self{
|
||||
Self(rbx_types::NumberRange{min,max})
|
||||
}
|
||||
}
|
||||
impl From<NumberRange> for rbx_types::NumberRange{
|
||||
fn from(NumberRange(value):NumberRange)->rbx_types::NumberRange{
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
let table=lua.create_table()?;
|
||||
|
||||
table.raw_set("new",
|
||||
lua.create_function(|_,(min,max):(Number,Option<Number>)|{
|
||||
Ok(match max{
|
||||
Some(max)=>NumberRange::new(min.into(),max.into()),
|
||||
None=>NumberRange::new(min.into(),min.into()),
|
||||
})
|
||||
})?
|
||||
)?;
|
||||
|
||||
globals.set("NumberRange",table)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl mlua::UserData for NumberRange{}
|
||||
type_from_lua_userdata_clone!(NumberRange);
|
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