Compare commits

..

9 Commits

Author SHA1 Message Date
d8804c0901 break to outer 2025-11-17 14:02:01 -08:00
865d1756b9 fix lint 2025-11-17 14:02:01 -08:00
3188f78744 document failure case 2025-11-17 14:02:01 -08:00
3e167af521 cool 2025-11-17 14:02:01 -08:00
8cf69933b1 wip 2025-11-17 14:02:01 -08:00
caef37dc36 wip 2025-11-17 14:02:01 -08:00
1832ce07ee wip new algorithm 2025-11-17 14:02:01 -08:00
5ac65ad845 new MeshQuery helper function 2025-11-17 14:02:01 -08:00
76696a3162 delete old algorithm 2025-11-17 14:02:01 -08:00
19 changed files with 753 additions and 856 deletions

642
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ impl<T:Copy> std::ops::Neg for &Body<T>{
impl<T> Body<T>
where Time<T>:Copy,
{
pub const ZERO:Self=Self::new(vec3::zero(),vec3::zero(),vec3::zero(),Time::ZERO);
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
Self{
position,
@@ -107,8 +107,8 @@ impl<T> Body<T>
self.time+=dt.into();
}
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::zero(){
if self.acceleration==vec3::zero(){
if self.velocity==vec3::ZERO{
if self.acceleration==vec3::ZERO{
None
}else{
Some(self.acceleration)

View File

@@ -90,9 +90,16 @@ pub trait MeshQuery{
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)
}
/// This must return a point inside the mesh.
#[expect(dead_code)]
fn hint_point(&self)->Planar64Vec3;
/// Returns an iterator over the vertices in the direction of the directed edges.
/// Intended to be used to find adjacent vertices:
/// `self.directed_verts(self.vert_edges(vert_id).as_ref())`
/// TODO: rewrite this function as `adjacent_vertices`
fn directed_verts(&self,edges:&[Self::Edge])->impl Iterator<Item=Self::Vert>{
edges.iter().map(|e|{
let edge_verts=self.edge_verts(e.as_undirected());
edge_verts.as_ref()[e.parity() as usize]
})
}
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)->impl AsRef<[Self::Edge]>;
@@ -450,15 +457,13 @@ impl MeshQuery for PhysicsMeshView<'_>{
let face_idx=self.topology.faces[face_id.get() as usize].get() as usize;
(self.data.faces[face_idx].normal,self.data.faces[face_idx].dot)
}
fn hint_point(&self)->Planar64Vec3{
// invariant: meshes always encompass the origin
vec3::zero()
}
//ideally I never calculate the vertex position, but I have to for the graphical meshes...
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
self.data.verts[vert_idx].0
}
/// Directed edges going clockwise when looking in the direction of the face normal.
/// (Edit this documentation if this is wrong!)
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.face_topology[face_id.get() as usize].edges.as_slice()
}
@@ -539,9 +544,6 @@ impl MeshQuery for TransformedMesh<'_>{
// wrap for speed
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
}
fn hint_point(&self)->Planar64Vec3{
self.transform.vertex.translation
}
#[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id)
@@ -624,17 +626,6 @@ pub struct MinkowskiMesh<'a>{
mesh1:TransformedMesh<'a>,
}
//infinity fev algorithm state transition
#[derive(Debug)]
enum Transition{
Done,//found closest vert, no edges are better
Vert(MinkowskiVert),//transition to vert
}
enum EV{
Vert(MinkowskiVert),
Edge(MinkowskiEdge),
}
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
let r=(time-relative_to).to_ratio();
@@ -651,122 +642,195 @@ impl MinkowskiMesh<'_>{
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
}
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).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.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(){
let distance_squared=diff.dot(diff);
if distance_squared<*best_distance_squared{
best_transition=Transition::Vert(test_vert_id);
*best_distance_squared=distance_squared;
}
}
}
best_transition
}
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).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
if edge_n.dot(infinity_dir).is_zero(){
let d=edge_n.dot(diff);
//test the edge
let edge_nn=edge_n.dot(edge_n);
if !d.is_negative()&&d<=edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
//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());
*best_distance_squared=distance_squared;
}
}
}
}
best_transition
}
fn crawl_boundaries(&self,mut vert_id:MinkowskiVert,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
fn closest_fev_not_inside(&self,relative_position:Planar64Vec3)->Option<FEV<MinkowskiMesh<'_>>>{
// Make a fast guess as to what the closest point will be.
let MinkowskiVert::VertVert(mut v0,mut v1)=self.farthest_vert(relative_position);
// TODO: alternate naive vertex searches to improve the robustness
// in the "tall bipyramid" failure case
let mut m0v=self.mesh0.vert(v0);
let mut m1v=self.mesh1.vert(v1);
let mut best_distance_squared={
let diff=point-self.vert(vert_id);
let diff=relative_position+m1v-m0v;
diff.dot(diff)
};
let mut v0e=self.mesh0.vert_edges(v0);
let mut v1e=self.mesh1.vert_edges(v1);
let mut v0e_ref=v0e.as_ref();
let mut v1e_ref=v1e.as_ref();
// repeatedly check adjacent vertex permutations to see if they are closer
loop{
match self.next_transition_vert(vert_id,&mut best_distance_squared,infinity_dir,point){
Transition::Done=>return self.final_ev(vert_id,&mut best_distance_squared,infinity_dir,point),
Transition::Vert(new_vert_id)=>vert_id=new_vert_id,
let mut best_v0=None;
let mut best_v1=None;
// check vertices adjacent to v1 against v0
for m1_test_vert in self.mesh1.directed_verts(v1e_ref){
let m1v_test=self.mesh1.vert(m1_test_vert);
let diff=relative_position+m1v_test-m0v;
let d=diff.dot(diff);
if d<best_distance_squared{
best_distance_squared=d;
best_v0=None;
best_v1=Some(m1_test_vert);
}
}
}
}
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh<'_>>{
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if the boundary is not crossable and we are on the wrong side
let edge_n=self.edge_n(edge_id);
// point is multiplied by two because vert_sum sums two vertices.
let delta_pos=point*2-{
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).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);
let boundary_d=boundary_n.dot(delta_pos);
//check if time of collision is outside Time::MIN..Time::MAX
//infinity_dir can always be treated as a velocity
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
//both faces cannot pass this condition, return early if one does.
return FEV::Face(face_id);
// check vertices adjacent to v0 against v1
for m0_test_vert in self.mesh0.directed_verts(v0e_ref){
let m0v_test=self.mesh0.vert(m0_test_vert);
let diff=relative_position+m1v-m0v_test;
let d=diff.dot(diff);
if d<best_distance_squared{
best_distance_squared=d;
best_v0=Some(m0_test_vert);
best_v1=None;
}
}
// check permutations of adjacent vertices
for m0_test_vert in self.mesh0.directed_verts(v0e_ref){
let m0v_test=self.mesh0.vert(m0_test_vert);
for m1_test_vert in self.mesh1.directed_verts(v1e_ref){
let m1v_test=self.mesh1.vert(m1_test_vert);
let diff=relative_position+m1v_test-m0v_test;
let d=diff.dot(diff);
if d<best_distance_squared{
best_distance_squared=d;
best_v0=Some(m0_test_vert);
best_v1=Some(m1_test_vert);
}
}
FEV::Edge(edge_id)
},
}
// end condition
let v0_changed=match best_v0{
Some(new_v0)=>{
v0=new_v0;
m0v=self.mesh0.vert(v0);
v0e=self.mesh0.vert_edges(v0);
v0e_ref=v0e.as_ref();
true
},
None=>false,
};
let v1_changed=match best_v1{
Some(new_v1)=>{
v1=new_v1;
m1v=self.mesh1.vert(v1);
v1e=self.mesh1.vert_edges(v1);
v1e_ref=v1e.as_ref();
true
},
None=>false,
};
// if neither vertex changes, we found the closest two vertices.
if !(v0_changed&&v1_changed){
break;
}
}
}
// TODO: fundamentally improve this algorithm.
// All it needs to do is find the closest point on the mesh
// and return the FEV which the point resides on.
//
// What it actually does is use the above functions to trace a ray in from infinity,
// crawling the closest point along the mesh surface until the ray reaches
// the starting point to discover the final FEV.
//
// The actual collision prediction probably does a single test
// and then immediately returns with 0 FEV transitions on average,
// because of the strict time_limit constraint.
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Bound<&Time>)->Option<FEV<MinkowskiMesh<'_>>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::zero();
//crawl in from negative infinity along a tangent line to get the closest fev
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,start_time).miss()
})
// now you have the two closest vertices
let mut best_fev=FEV::Vert(MinkowskiVert::VertVert(v0,v1));
// ==== FEV::Edge ====
// test VertEdges
for &e1 in v1e_ref{
let edge_verts=self.mesh1.edge_verts(e1.as_undirected());
let &[ev0_id,ev1_id]=edge_verts.as_ref();
let (ev0,ev1)=(self.mesh1.vert(ev0_id),self.mesh1.vert(ev1_id));
let edge_n=ev1-ev0;
// use relative coordinates to make including relative_position easier
let diff=relative_position+m0v-ev0;
let d=diff.dot(edge_n);
// is test point between edge vertices
let edge_nn=edge_n.dot(edge_n);
if d.is_positive()&&d<edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_2()
};
if distance_squared<best_distance_squared{
best_distance_squared=distance_squared;
best_fev=FEV::Edge(MinkowskiEdge::VertEdge(v0,e1.as_undirected()))
}
}
}
// test EdgeVerts
for &e0 in v0e_ref{
let edge_verts=self.mesh0.edge_verts(e0.as_undirected());
let &[ev0_id,ev1_id]=edge_verts.as_ref();
let (ev0,ev1)=(self.mesh0.vert(ev0_id),self.mesh0.vert(ev1_id));
let edge_n=ev1-ev0;
// use relative coordinates to make including relative_position easier
let diff=m1v-relative_position-ev0;
let d=diff.dot(edge_n);
// is test point between edge vertices
let edge_nn=edge_n.dot(edge_n);
if d.is_positive()&&d<edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_2()
};
if distance_squared<best_distance_squared{
best_distance_squared=distance_squared;
best_fev=FEV::Edge(MinkowskiEdge::EdgeVert(e0.as_undirected(),v1))
}
}
}
// ==== FEV::Face ====
// test VertFaces
'outer: for &f1 in self.mesh1.vert_faces(v1).as_ref(){
let (n,d)=self.mesh1.face_nd(f1);
// Test the face's voronoi column
for &e1 in self.mesh1.face_edges(f1).as_ref(){
let edge_n=self.mesh1.directed_edge_n(e1);
let boundary_n=n.cross(edge_n);
let &[ev0_id,ev1_id]=self.mesh1.edge_verts(e1.as_undirected()).as_ref();
let (ev0,ev1)=(self.mesh1.vert(ev0_id),self.mesh1.vert(ev1_id));
let diff=(relative_position+m0v)*2-(ev0+ev1);
if boundary_n.dot(diff).is_negative(){
// The test point is outside the face's voronoi column.
continue 'outer;
}
}
// Calculate distance
let d=n.dot(relative_position+m0v)-d;
// Wrap for speed
let distance_squared=(d*d).wrap_2();
if distance_squared<best_distance_squared{
best_distance_squared=distance_squared;
best_fev=FEV::Face(MinkowskiFace::VertFace(v0,f1));
}
}
// test FaceVerts
'outer: for &f0 in self.mesh0.vert_faces(v0).as_ref(){
let (n,d)=self.mesh0.face_nd(f0);
// Test the face's voronoi column
for &e0 in self.mesh0.face_edges(f0).as_ref(){
let edge_n=self.mesh0.directed_edge_n(e0);
let boundary_n=n.cross(edge_n);
let &[ev0_id,ev1_id]=self.mesh0.edge_verts(e0.as_undirected()).as_ref();
let (ev0,ev1)=(self.mesh0.vert(ev0_id),self.mesh0.vert(ev1_id));
let diff=(m1v-relative_position)*2-(ev0+ev1);
if boundary_n.dot(diff).is_negative(){
// The test point is outside the face's voronoi column.
continue 'outer;
}
}
// Calculate distance
let d=n.dot(relative_position+m0v)-d;
// Wrap for speed
let distance_squared=(d*d).wrap_2();
if distance_squared<best_distance_squared{
best_distance_squared=distance_squared;
best_fev=FEV::Face(MinkowskiFace::FaceVert(f0,v1));
}
}
// test EdgeEdges
Some(best_fev)
}
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(*relative_body,range.start_bound()).and_then(|fev|{
self.closest_fev_not_inside(relative_body.position).and_then(|fev|{
//continue forwards along the body parabola
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
})
@@ -775,10 +839,9 @@ impl MinkowskiMesh<'_>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let infinity_body=-relative_body;
self.closest_fev_not_inside(infinity_body,lower_bound.as_ref()).and_then(|fev|{
self.closest_fev_not_inside(relative_body.position).and_then(|fev|{
//continue backwards along the body parabola
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
fev.crawl(self,&-relative_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time))
})
@@ -811,20 +874,10 @@ impl MinkowskiMesh<'_>{
}
best_edge
}
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
// Bound::Included means that the surface of the mesh is included in the mesh
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,Bound::Included(&infinity_body.time)).hit()
}
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::zero(),Time::ZERO);
//movement must escape the mesh forwards and backwards in time,
//otherwise the point is not inside the mesh
self.infinity_in(infinity_body)
.is_some_and(|_|
self.infinity_in(-infinity_body)
.is_some()
)
// TODO
println!("Unimplemented is_point_in_mesh called! {point}");
false
}
}
impl MeshQuery for MinkowskiMesh<'_>{
@@ -863,10 +916,6 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn hint_point(&self)->Planar64Vec3{
self.mesh1.transform.vertex.translation-
self.mesh0.transform.vertex.translation
}
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{

File diff suppressed because it is too large Load Diff

View File

@@ -39,18 +39,20 @@ impl Contact{
//note that this is horrible with fixed point arithmetic
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
let det=c0.normal.dot(c0.velocity);
if det.abs()==Fixed::ZERO{
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
Some(c0.normal*d0/det)
}
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
let u0_u1=c0.velocity.cross(c1.velocity);
let n0_n1=c0.normal.cross(c1.normal);
let det=u0_u1.dot(n0_n1);
if det.abs()==Fixed::ZERO{
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
@@ -58,9 +60,10 @@ fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,1
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
}
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs()==Fixed::ZERO{
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
@@ -146,7 +149,7 @@ fn is_space_enclosed_4(
}
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
Ray{origin:point,direction:vec3::zero()}
Ray{origin:point,direction:vec3::ZERO}
}
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
//wrap for speed
@@ -318,13 +321,13 @@ mod tests{
fn test_push_solve(){
let contacts=vec![
Contact{
position:vec3::zero(),
position:vec3::ZERO,
velocity:vec3::Y,
normal:vec3::Y,
}
];
assert_eq!(
vec3::zero(),
vec3::ZERO,
push_solve(&contacts,vec3::NEG_Y)
);
}

View File

@@ -77,7 +77,7 @@ fn simultaneous_collision(){
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
physics.style_mut().gravity=vec3::zero();
physics.style_mut().gravity=vec3::ZERO;
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(2))
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
// the order that they hit does matter, but we aren't currently worrying about that.
@@ -101,7 +101,7 @@ fn bug_3(){
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
physics.style_mut().gravity=vec3::zero();
physics.style_mut().gravity=vec3::ZERO;
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(3))
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
// touch side of part at 0,0,0

View File

@@ -210,7 +210,7 @@ pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
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 normal=mb.acquire_normal_id(integer::vec3::ZERO);
let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{

View File

@@ -105,7 +105,7 @@ pub fn convert<'a>(
water:Some(attr::IntersectingWater{
viscosity:integer::Planar64::ONE,
density:integer::Planar64::ONE,
velocity:integer::vec3::zero(),
velocity:integer::vec3::ZERO,
}),
},
general:attr::GeneralAttributes::default(),
@@ -295,7 +295,7 @@ pub fn convert<'a>(
attributes,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
integer::vec3::zero(),
integer::vec3::ZERO,
),
color:glam::Vec4::ONE,
});

View File

@@ -2,6 +2,7 @@ const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16
use crate::integer::{int,vec3::int as int3,AbsoluteTime,Ratio64,Planar64,Planar64Vec3};
use crate::controls_bitflag::Controls;
use crate::physics::Time as PhysicsTime;
#[derive(Clone,Debug)]
pub struct StyleModifiers{
@@ -272,6 +273,9 @@ impl StrafeSettings{
false=>None,
}
}
pub fn next_tick(&self,time:PhysicsTime)->PhysicsTime{
PhysicsTime::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
}
pub const fn activates(&self,controls:Controls)->bool{
self.enable.activates(controls)
}
@@ -315,7 +319,7 @@ impl WalkSettings{
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(){
if control_dir==crate::integer::vec3::ZERO{
return control_dir;
}
let nn=normal.length_squared();
@@ -325,13 +329,13 @@ impl WalkSettings{
let dd=d*d;
if dd<nnmm{
let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::zero(){
crate::integer::vec3::zero()
if cr==crate::integer::vec3::ZERO_2{
crate::integer::vec3::ZERO
}else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
}
}else{
crate::integer::vec3::zero()
crate::integer::vec3::ZERO
}
}
pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
@@ -356,7 +360,7 @@ impl LadderSettings{
self.accelerate.accel
}
pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
if control_dir==crate::integer::vec3::zero(){
if control_dir==crate::integer::vec3::ZERO{
return control_dir;
}
let nn=normal.length_squared();
@@ -378,13 +382,13 @@ impl LadderSettings{
//- fix the underlying issue
if dd<nnmm{
let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::zero(){
crate::integer::vec3::zero()
if cr==crate::integer::vec3::ZERO_2{
crate::integer::vec3::ZERO
}else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
}
}else{
crate::integer::vec3::zero()
crate::integer::vec3::ZERO
}
}
}

View File

@@ -561,6 +561,12 @@ pub mod vec3{
pub use linear_ops::types::Vector3;
pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]);
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
pub const ZERO_2:Vector3<Fixed::<2,64>>=Vector3::new([Fixed::<2,64>::ZERO;3]);
pub const ZERO_3:Vector3<Fixed::<3,96>>=Vector3::new([Fixed::<3,96>::ZERO;3]);
pub const ZERO_4:Vector3<Fixed::<4,128>>=Vector3::new([Fixed::<4,128>::ZERO;3]);
pub const ZERO_5:Vector3<Fixed::<5,160>>=Vector3::new([Fixed::<5,160>::ZERO;3]);
pub const ZERO_6:Vector3<Fixed::<6,192>>=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]);
@@ -569,10 +575,6 @@ pub mod vec3{
pub const NEG_Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::NEG_ONE,Planar64::ZERO]);
pub const NEG_Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::NEG_ONE]);
pub const NEG_ONE:Planar64Vec3=Planar64Vec3::new([Planar64::NEG_ONE,Planar64::NEG_ONE,Planar64::NEG_ONE]);
// TODO: use #![feature(generic_const_items)] when stabilized https://github.com/rust-lang/rust/issues/113521
pub const fn zero<const N:usize,const F:usize>()->Vector3<Fixed<N,F>>{
Vector3::new([Fixed::ZERO;3])
}
#[inline]
pub const fn int(x:i32,y:i32,z:i32)->Planar64Vec3{
Planar64Vec3::new([Planar64::raw((x as i64)<<32),Planar64::raw((y as i64)<<32),Planar64::raw((z as i64)<<32)])
@@ -661,7 +663,7 @@ pub struct Planar64Affine3{
pub translation:Planar64Vec3,
}
impl Planar64Affine3{
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::zero());
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO);
#[inline]
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
Self{matrix3,translation}

View File

@@ -70,34 +70,6 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
pub const fn midpoint(self,other:Self)->Self{
Self::from_bits(self.bits.midpoint(other.bits))
}
#[inline]
pub const fn min(self,other:Self)->Self{
Self::from_bits(self.bits.min(other.bits))
}
#[inline]
pub const fn max(self,other:Self)->Self{
Self::from_bits(self.bits.max(other.bits))
}
/// return the result of self*sign(other)
#[inline]
pub const fn mul_sign<const N1:usize,const F1:usize>(self,other:Fixed<N1,F1>)->Self{
if other.is_negative(){
Self::from_bits(self.bits.neg())
}else if other.is_zero(){
Fixed::ZERO
}else{
self
}
}
/// return the result of self/sign(other) (divide by zero does not change the sign)
#[inline]
pub const fn div_sign<const N1:usize,const F1:usize>(self,other:Fixed<N1,F1>)->Self{
if other.is_negative(){
Self::from_bits(self.bits.neg())
}else{
self
}
}
}
impl<const F:usize> Fixed<1,F>{
/// My old code called this function everywhere so let's provide it
@@ -129,6 +101,28 @@ impl_from!(
i8,i16,i32,i64,i128,isize
);
impl<const N:usize,const F:usize,T> PartialEq<T> for Fixed<N,F>
where
T:Copy,
BInt::<N>:From<T>,
{
#[inline]
fn eq(&self,&other:&T)->bool{
self.bits.eq(&other.into())
}
}
impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
where
T:Copy,
BInt::<N>:From<T>,
{
#[inline]
fn partial_cmp(&self,&other:&T)->Option<std::cmp::Ordering>{
self.bits.partial_cmp(&other.into())
}
}
impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
type Output=Self;
#[inline]
@@ -334,6 +328,16 @@ macro_rules! impl_additive_operator {
self.$method(other)
}
}
impl<const N:usize,const F:usize,U> core::ops::$trait<U> for $struct<N,F>
where
BInt::<N>:From<U>,
{
type Output = $output;
#[inline]
fn $method(self, other: U) -> Self::Output {
Self::from_bits(self.bits.$method(BInt::<N>::from(other).shl(F as u32)))
}
}
};
}
macro_rules! impl_additive_assign_operator {
@@ -344,6 +348,15 @@ macro_rules! impl_additive_assign_operator {
self.bits.$method(other.bits);
}
}
impl<const N:usize,const F:usize,U> core::ops::$trait<U> for $struct<N,F>
where
BInt::<N>:From<U>,
{
#[inline]
fn $method(&mut self, other: U) {
self.bits.$method(BInt::<N>::from(other).shl(F as u32));
}
}
};
}
@@ -366,7 +379,7 @@ impl_additive_operator!( Fixed, BitXor, bitxor, Self );
// non-wide operators. The result is the same width as the inputs.
// This macro is not used in the default configuration.
#[expect(unused_macros)]
#[allow(unused_macros)]
macro_rules! impl_multiplicative_operator_not_const_generic {
( ($struct: ident, $trait: ident, $method: ident, $output: ty ), $width:expr ) => {
impl<const F:usize> core::ops::$trait for $struct<$width,F>{
@@ -545,7 +558,7 @@ impl_shift_operator!( Fixed, Shr, shr, Self );
// wide operators. The result width is the sum of the input widths, i.e. none of the multiplication
#[expect(unused_macros)]
#[allow(unused_macros)]
macro_rules! impl_wide_operators{
($lhs:expr,$rhs:expr)=>{
impl core::ops::Mul<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{

View File

@@ -13,16 +13,16 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
bytemuck = "1.14.3"
glam = "0.30.0"
regex = { version = "1.11.3", default-features = false }
rbx_binary = { version = "1.0.1-sn5", registry = "strafesnet" }
rbx_dom_weak = { version = "3.0.1-sn5", registry = "strafesnet" }
rbx_mesh = "0.5.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.0.1-sn5", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.5.1", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
rbx_binary = "2.0.1"
rbx_dom_weak = "4.1.0"
rbx_reflection = "6.1.0"
rbx_reflection_database = "2.0.2"
rbx_xml = "2.0.1"
[lints]
workspace = true

View File

@@ -251,7 +251,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}
}
//need some way to skip this
if allow_booster&&velocity!=vec3::zero(){
if allow_booster&&velocity!=vec3::ZERO{
general.booster=Some(attr::Booster::Velocity(velocity));
}
Ok(match force_can_collide{
@@ -559,7 +559,7 @@ pub fn convert<'a>(
//just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
let db=rbx_reflection_database::get().unwrap();
let db=rbx_reflection_database::get();
let basepart=&db.classes["BasePart"];
let baseparts=dom.descendants().filter(|&instance|
db.classes.get(instance.class.as_str()).is_some_and(|class|

View File

@@ -250,7 +250,7 @@ pub fn convert(
// generate a unit cube as default physics
let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
let normal=mb.acquire_normal_id(vec3::zero());
let normal=mb.acquire_normal_id(vec3::ZERO);
let color=mb.acquire_color_id(glam::Vec4::ONE);
let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})

View File

@@ -1,6 +1,6 @@
[package]
name = "roblox_emulator"
version = "0.5.2"
version = "0.5.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -15,10 +15,10 @@ run-service=[]
glam = "0.30.0"
mlua = { version = "0.11.3", features = ["luau"] }
phf = { version = "0.13.1", features = ["macros"] }
rbx_dom_weak = "4.1.0"
rbx_reflection = "6.1.0"
rbx_reflection_database = "2.0.2"
rbx_types = "3.1.0"
rbx_dom_weak = { version = "3.0.1-sn5", registry = "strafesnet" }
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_types = "2.0.0"
[lints]
workspace = true

View File

@@ -52,7 +52,7 @@ impl Context{
}
/// 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{
let db=rbx_reflection_database::get().unwrap();
let db=rbx_reflection_database::get();
let Some(superclass)=db.classes.get(superclass)else{
panic!("Invalid class");
};

View File

@@ -37,7 +37,7 @@ impl PartialEq for EnumItem<'_>{
pub struct Enums;
impl Enums{
pub fn get(&self,index:&str)->Option<EnumItems<'static>>{
let db=rbx_reflection_database::get().unwrap();
let db=rbx_reflection_database::get();
db.enums.get(index).map(|ed|EnumItems{ed})
}
}

View File

@@ -37,7 +37,7 @@ pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>
}
pub fn class_is_a(class:&str,superclass:&str)->bool{
let db=rbx_reflection_database::get().unwrap();
let db=rbx_reflection_database::get();
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
return false;
};
@@ -80,14 +80,14 @@ pub fn find_first_descendant_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak
}
pub fn find_first_child_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get().unwrap();
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 WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get().unwrap();
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))
@@ -282,7 +282,7 @@ impl mlua::UserData for Instance{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
//println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get().unwrap();
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
// Interestingly, ustr can know ahead of time if
@@ -344,7 +344,7 @@ impl mlua::UserData for Instance{
let index_str=&*index.to_str()?;
dom_mut(lua,|dom|{
let instance=this.get_mut(dom)?;
let db=rbx_reflection_database::get().unwrap();
let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let property=db.superclasses_iter(class).find_map(|cls|
cls.properties.get(index_str)

View File

@@ -13,10 +13,10 @@ futures = "0.3.31"
image = "0.25.2"
image_dds = "0.7.1"
rbx_asset = { version = "0.5.0", registry = "strafesnet" }
rbx_binary = "2.0.1"
rbx_dom_weak = "4.1.0"
rbx_reflection_database = "2.0.2"
rbx_xml = "2.0.1"
rbx_binary = { version = "1.0.1-sn5", registry = "strafesnet" }
rbx_dom_weak = { version = "3.0.1-sn5", registry = "strafesnet" }
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.0.1-sn5", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.3.1", path = "../lib/bsp_loader", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../lib/deferred_loader", registry = "strafesnet" }