Compare commits

...

34 Commits

Author SHA1 Message Date
96e311ef99 surfs work better when you do filter this bevel thing 2025-02-14 11:58:53 -08:00
71d717a9d8 skip mesh generation for tumor triggers 2025-02-14 11:58:53 -08:00
4d17e50883 spawn model intersect attribute 2025-02-14 11:52:08 -08:00
1024d7564f you spawn at the start of the map (using vbsp fork) 2025-02-14 11:52:08 -08:00
ddedb56a3b common: integer: Planar64Affine3::from_translation 2025-02-14 11:41:33 -08:00
10ca30db2e bsp_loader: split brush functions so I can be lazy 2025-02-14 11:41:33 -08:00
eec6a1fa72 infinite loop detect 2025-02-14 11:41:33 -08:00
d384744d2d refactor world_models generation 2025-02-14 11:41:33 -08:00
24c01962bf only create physics for SOLID brushes 2025-02-14 11:41:33 -08:00
dd4f81a73c transform normals 2025-02-14 11:41:33 -08:00
f46a7ba458 test degenerate face 2025-02-14 11:41:33 -08:00
97ddf94df4 remaining faces not needed, it was just wrong (prev commit) 2025-02-14 11:41:33 -08:00
d654f79748 I fix the algorithm 2025-02-14 11:41:33 -08:00
091e8a4453 don't split ratio 2025-02-14 11:41:33 -08:00
b7a86467d8 a bug 2025-02-14 11:41:33 -08:00
c9c6ddbf74 fix stupid 2025-02-14 11:41:33 -08:00
96af3fd1a1 generate it 2025-02-14 11:41:33 -08:00
b6c40c3470 clarify and comment 2025-02-14 11:41:33 -08:00
0418b83eaf no revisit 2025-02-14 11:41:33 -08:00
d138be2acd unused 2025-02-14 11:41:33 -08:00
42caa785ce there could be problem 2025-02-14 11:41:33 -08:00
5b5c347c7a we are doing exact calculations and don't need an epsilon 2025-02-14 11:41:33 -08:00
a8853cb1a7 don't valve transform normals 2025-02-14 11:41:33 -08:00
2c4ec76c96 PlanesToFacesError 2025-02-14 11:41:33 -08:00
af3f9cf1fe deduplicate ahead of time 2025-02-14 11:41:33 -08:00
067b5eee3c THE ALGORITHM 2025-02-14 11:41:33 -08:00
0b63e06ad2 we interutiosn ptow neffinrmenbs 2025-02-14 11:41:33 -08:00
6c40a51cd2 outline 2025-02-14 11:41:33 -08:00
80f28e8bf7 more how 2025-02-14 11:41:33 -08:00
d6d1c5365b this is how to do 2025-02-14 11:41:33 -08:00
0542fad5dd gotta generate mesh from planes 2025-02-14 11:41:33 -08:00
c7901d58d8 vert pos 2025-02-14 11:41:33 -08:00
4506add978 generate physics from brushes 2025-02-14 11:41:33 -08:00
5cf2272849 rename TEMP_TOUCH_ME_ATTRIBUTE 2025-02-14 11:41:33 -08:00
10 changed files with 455 additions and 45 deletions

8
Cargo.lock generated

@ -1867,7 +1867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@ -4370,9 +4370,9 @@ dependencies = [
[[package]]
name = "vbsp"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14a5685e0bb386aac9b9c6046a05152a46a0bc58d53afb3fbe577f1a1c2bb05"
version = "0.7.0-codegen1"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "ff3887880f15dc530860d683aa6cf33f2e03e5a1c2783e3dc8252bffbf889c3c"
dependencies = [
"ahash",
"arrayvec",

@ -13,6 +13,6 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
glam = "0.29.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.6.0"
vbsp = { version = "0.7.0-codegen1", registry = "strafesnet" }
vmdl = "0.2.0"
vpk = "0.2.0"

321
lib/bsp_loader/src/brush.rs Normal file

@ -0,0 +1,321 @@
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,
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 face0, face1 until all faces form part of the convex solid
'find: loop{
if let Some(a)=detect_loop.checked_sub(1){
detect_loop=a;
}else{
return Err(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.fix_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.fix_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.fix_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;
}
if let Some(new_intersection)=solve3(new_face,face1,face2){
// face0 does not occlude (or intersect) the new intersection
if (face0.dot.fix_2()/Planar64::ONE).lt_ratio(face0.normal.dot(new_intersection.num)/new_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().fix_1());
// 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;
if let Some(a)=detect_loop.checked_sub(1){
detect_loop=a;
}else{
return Err(PlanesToFacesError::InfiniteLoop2);
}
}
faces.push(face);
}
if faces.is_empty(){
Err(PlanesToFacesError::EmptyFaces)
}else{
Ok(Faces{
faces,
})
}
}
#[derive(Debug)]
pub enum BrushToMeshError{
SliceBrushSides,
MissingPlane,
InvalidFaceCount{
count:usize,
},
InvalidPlanes(PlanesToFacesError),
SkipBecauseTexture,
}
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)?;
for side in sides{
if let Some(texture_info)=bsp.textures_info.get(side.texture_info as usize){
let texture_info=vbsp::Handle::new(bsp,texture_info);
let s=texture_info.name();
if s.starts_with("tools/")||s.starts_with("TOOLS/"){
return Err(BrushToMeshError::SkipBecauseTexture);
}
}
}
let face_list=sides.iter().filter(|side|side.bevel==0).map(|side|{
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();
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();
dbg!(faces);
}
}

@ -41,7 +41,11 @@ pub fn convert<'a>(
//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);
unique_attributes.push(gameplay_attributes::CollisionAttributes::contact_default());
unique_attributes.push(gameplay_attributes::CollisionAttributes::intersect_default());
const ATTRIBUTE_DECORATION:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);
const ATTRIBUTE_CONTACT_DEFAULT:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(1);
const ATTRIBUTE_INTERSECT_DEFAULT:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(2);
//declare all prop models to Loader
let prop_models=bsp.static_props().map(|prop|{
@ -50,7 +54,7 @@ pub fn convert<'a>(
let placement=prop.as_prop_placement();
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))
@ -67,7 +71,7 @@ pub fn convert<'a>(
//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|{
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);
@ -118,49 +122,114 @@ pub fn convert<'a>(
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,
});
for raw_ent in bsp.entities.iter(){
match raw_ent.parse(){
Ok(vbsp::basic::Entity::Brush(brush))
|Ok(vbsp::basic::Entity::BrushIllusionary(brush))
|Ok(vbsp::basic::Entity::BrushWall(brush))
|Ok(vbsp::basic::Entity::BrushWallToggle(brush))=>{
//The first character of brush.model is '*'
match brush.model[1..].parse(){
Ok(mesh_id)=>{
world_models.push(model::Model{
mesh:model::MeshId::new(mesh_id),
attributes:ATTRIBUTE_DECORATION,
transform:integer::Planar64Affine3::from_translation(
valve_transform(brush.origin.into())
),
color:(glam::Vec3::from_array([
brush.color.r as f32,
brush.color.g as f32,
brush.color.b as f32
])/255.0).extend(1.0),
});
},
Err(e)=>{
println!("Brush model int parse error: {e}");
},
}
},
_=>(),
}
}).collect();
match raw_ent.parse(){
Ok(vbsp::css::Entity::InfoPlayerCounterterrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Err(e)=>{
println!("Bsp Entity parse error: {e}");
},
_=>(),
}
}
// physics models
for brush in &bsp.brushes{
if !brush.flags.contains(vbsp::BrushFlags::SOLID){
continue;
}
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:ATTRIBUTE_CONTACT_DEFAULT,
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=strafesnet_common::gameplay_modes::Stage::empty(model_id);
let main_mode=strafesnet_common::gameplay_modes::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(main_mode);
}
PartialMap1{
attributes:unique_attributes,
world_meshes,
prop_models,
world_models,
modes:strafesnet_common::gameplay_modes::Modes::new(Vec::new()),
modes:strafesnet_common::gameplay_modes::Modes::new(modes_list),
}
}

@ -2,9 +2,16 @@ use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLo
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()
}

@ -171,4 +171,7 @@ impl CollisionAttributes{
pub fn contact_default()->Self{
Self::Contact(ContactAttributes::default())
}
pub fn intersect_default()->Self{
Self::Intersect(IntersectAttributes::default())
}
}

@ -654,11 +654,19 @@ 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
}

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

@ -25,7 +25,7 @@ strafesnet_rbx_loader = { version = "0.6.0", path = "../lib/rbx_loader", registr
strafesnet_snf = { version = "0.3.0", path = "../lib/snf", registry = "strafesnet" }
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = "0.6.0"
vbsp = { version = "0.7.0-codegen1", registry = "strafesnet" }
vmdl = "0.2.0"
vmt-parser = "0.2.0"
vpk = "0.2.0"