Compare commits

...

11 Commits

Author SHA1 Message Date
a92526407c a 2026-03-25 08:20:20 -07:00
47239b10a8 print 2026-03-25 08:16:46 -07:00
f5588989e9 Revert "remove unhelpful debug prints"
This reverts commit f17648e7fa.
2026-03-25 08:16:46 -07:00
979d46e42a print crawl_to output 2026-03-25 08:16:46 -07:00
f858fa86e6 p 2026-03-25 08:16:46 -07:00
8e6d598ea3 debug 2026-03-25 08:16:46 -07:00
78c0cab05a debug 2026-03-25 08:16:11 -07:00
dd972c91ee debug 2026-03-25 08:15:40 -07:00
8a897ca377 hack in md lua 2026-03-25 08:15:40 -07:00
65f29fd395 no ignore 2026-03-25 08:14:08 -07:00
a673d62ffd bug 26 2026-03-25 08:14:08 -07:00
9 changed files with 251 additions and 9 deletions

1
Cargo.lock generated
View File

@@ -3997,6 +3997,7 @@ dependencies = [
"arrayvec",
"glam",
"id",
"mlua",
"strafesnet_common",
]

View File

@@ -7,6 +7,7 @@ edition = "2024"
arrayvec = "0.7.6"
glam.workspace = true
id = { version = "0.1.0", registry = "strafesnet" }
mlua = { version = "0.11.5", features = ["luau"] }
strafesnet_common.workspace = true
[lints]

View File

@@ -5,6 +5,7 @@ mod minkowski;
mod model;
mod push_solve;
mod minimum_difference;
mod minimum_difference_lua;
pub mod physics;

View File

@@ -11,7 +11,7 @@ use crate::minkowski::{MinkowskiMesh,MinkowskiVert};
// written by Trey Reynolds in 2021
type Simplex<const N:usize,Vert>=[Vert;N];
#[derive(Clone,Copy)]
#[derive(Clone,Copy,Debug)]
enum Simplex1_3<Vert>{
Simplex1(Simplex<1,Vert>),
Simplex2(Simplex<2,Vert>),
@@ -139,12 +139,15 @@ fn reduce1<M:MeshQuery<Position=Planar64Vec3>>(
)->Reduced<M::Vert>
where M::Vert:Copy,
{
println!("reduce1");
// --debug.profilebegin("reduceSimplex0")
// local a = a1 - a0
let p0=mesh.vert(v0);
println!("p0={p0}");
// local p = -a
let p=-(p0+point);
println!("p={p}");
// local direction = p
let mut dir=p;
@@ -171,6 +174,7 @@ fn reduce2<M:MeshQuery<Position=Planar64Vec3>>(
where
M::Vert:Copy
{
println!("reduce2");
// --debug.profilebegin("reduceSimplex1")
// local a = a1 - a0
// local b = b1 - b0
@@ -231,6 +235,7 @@ fn reduce3<M:MeshQuery<Position=Planar64Vec3>>(
where
M::Vert:Copy
{
println!("reduce3");
// --debug.profilebegin("reduceSimplex2")
// local a = a1 - a0
// local b = b1 - b0
@@ -343,6 +348,7 @@ fn reduce4<M:MeshQuery<Position=Planar64Vec3>>(
where
M::Vert:Copy
{
println!("reduce4");
// --debug.profilebegin("reduceSimplex3")
// local a = a1 - a0
// local b = b1 - b0
@@ -734,6 +740,7 @@ fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiV
//if test point is behind face, the face is invalid
// TODO: find out why I thought of this backwards
if !(face_n.dot(point)-d).is_positive(){
println!("behind");
continue;
}
//edge-face boundary nd, n facing out of the face towards the edge
@@ -743,6 +750,8 @@ fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiV
if !boundary_d.is_positive(){
//both faces cannot pass this condition, return early if one does.
return FEV::Face(face_id);
}else{
println!("boundary_d is positive");
}
}
FEV::Edge(edge_id)
@@ -751,11 +760,21 @@ fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiV
}
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
println!("=== LUA ===");
let (hits,_details)=crate::minimum_difference_lua::minimum_difference_details(mesh,point).unwrap();
println!("=== RUST ===");
let closest_fev_not_inside=closest_fev_not_inside_inner(mesh,point);
assert_eq!(hits,closest_fev_not_inside.is_none(),"algorithms disagree");
closest_fev_not_inside
}
pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
const ENABLE_FAST_FAIL:bool=false;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,simplex|{
println!("on_exact simplex={simplex:?}");
if is_intersecting{
return None;
}
@@ -766,7 +785,11 @@ pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->O
Simplex1_3::Simplex2([v0,v1])=>{
// invert
let (v0,v1)=(-v0,-v1);
crawl_to_closest_ev(mesh,[v0,v1],point).into()
let ev=crawl_to_closest_ev(mesh,[v0,v1],point);
if !matches!(ev,EV::Edge(_)){
println!("I can't believe it's not an edge!");
}
ev.into()
},
Simplex1_3::Simplex3([v0,v1,v2])=>{
// invert
@@ -774,7 +797,11 @@ pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->O
// Shimmy to the side until you find a face that contains the closest point
// it's ALWAYS representable as a face, but this algorithm may
// return E or V in edge cases but I don't think that will break the face crawler
crawl_to_closest_fev(mesh,[v0,v1,v2],point)
let fev=crawl_to_closest_fev(mesh,[v0,v1,v2],point);
if !matches!(fev,FEV::Face(_)){
println!("I can't believe it's not a face!");
}
fev
},
})
},
@@ -832,6 +859,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar6
if initial_axis==vec3::zero(){
initial_axis=choose_any_direction();
}
println!("initial_axis={initial_axis}");
let last_point=mesh.farthest_vert(-initial_axis);
// this represents the 'a' value in the commented code
let mut last_pos=mesh.vert(last_point);
@@ -840,6 +868,8 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar6
// exitRadius = testIntersection and 0 or exitRadius or 1/0
// for _ = 1, 100 do
loop{
println!("direction={direction}");
// new_point_p = queryP(-direction)
// new_point_q = queryQ(direction)
// local next_point = new_point_q - new_point_p
@@ -847,7 +877,11 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar6
let next_pos=mesh.vert(next_point);
// if -direction:Dot(next_point) > (exitRadius + radiusP + radiusQ)*direction.magnitude then
if ENABLE_FAST_FAIL&&direction.dot(next_pos+point).is_negative(){
let d=direction.dot(next_pos+point);
let fast_fail=d.is_negative();
println!("ENABLE_FAST_FAIL={ENABLE_FAST_FAIL} fast_fail={fast_fail} next_point={} dot={d}",next_pos+point);
if ENABLE_FAST_FAIL&&fast_fail{
println!("on_fast_fail");
return on_fast_fail();
}
@@ -856,8 +890,11 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar6
// if
// direction:Dot(next_point - a) <= 0 or
// absDet(next_point, a, b, c) < 1e-6
if !direction.dot(next_pos-last_pos).is_positive()
||simplex_big.det_is_zero(mesh){
let d1=direction.dot(next_pos-last_pos);
let cond2=simplex_big.det_is_zero(mesh);
println!("d1={d1:?} cond2={cond2}");
if !d1.is_positive()||cond2{
println!("on_exact");
// Found enough information to compute the exact closest point.
// local norm = direction.unit
// local dist = a:Dot(norm)
@@ -870,6 +907,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar6
match simplex_big.reduce(mesh,point){
// if a and b and c and d then
Reduce::Escape(simplex)=>{
println!("on_escape");
// Enough information to conclude that the meshes are intersecting.
// Topology information is computed if needed.
return on_escape(simplex);

View File

@@ -0,0 +1,174 @@
use mlua::{Lua,FromLuaMulti,IntoLuaMulti,Function,Result as LuaResult,Vector};
use strafesnet_common::integer::{Planar64,Planar64Vec3,FixedFromFloatError};
use crate::mesh_query::MeshQuery;
use crate::minkowski::MinkowskiMesh;
pub fn contains_point(
mesh:&MinkowskiMesh,
point:Planar64Vec3,
)->LuaResult<bool>{
Ok(minimum_difference(mesh,point,true)?.hits)
}
pub fn minimum_difference_details(
mesh:&MinkowskiMesh,
point:Planar64Vec3,
)->LuaResult<(bool,Option<Details>)>{
let md=minimum_difference(mesh,point,false)?;
Ok((md.hits,md.details))
}
fn p64v3(v:Vector)->Result<Planar64Vec3,FixedFromFloatError>{
Ok(Planar64Vec3::new([
v.x().try_into()?,
v.y().try_into()?,
v.z().try_into()?,
]))
}
fn vec(v:Planar64Vec3)->Vector{
Vector::new(v.x.into(),v.y.into(),v.z.into())
}
struct MinimumDifference{
hits:bool,
details:Option<Details>
}
pub struct Details{
pub distance:Planar64,
pub p_pos:Planar64Vec3,
pub p_norm:Planar64Vec3,
pub q_pos:Planar64Vec3,
pub q_norm:Planar64Vec3,
}
impl FromLuaMulti for MinimumDifference{
fn from_lua_multi(mut values:mlua::MultiValue,_lua:&Lua)->LuaResult<Self>{
match values.make_contiguous(){
&mut [
mlua::Value::Boolean(hits),
mlua::Value::Nil,
mlua::Value::Nil,
mlua::Value::Nil,
mlua::Value::Nil,
mlua::Value::Nil,
]=>Ok(Self{hits,details:None}),
&mut [
mlua::Value::Boolean(hits),
mlua::Value::Number(distance),
mlua::Value::Vector(p_pos),
mlua::Value::Vector(p_norm),
mlua::Value::Vector(q_pos),
mlua::Value::Vector(q_norm),
]=>Ok(Self{
hits,
details:Some(Details{
distance:distance.try_into().unwrap(),
p_pos:p64v3(p_pos).unwrap(),
p_norm:p64v3(p_norm).unwrap(),
q_pos:p64v3(q_pos).unwrap(),
q_norm:p64v3(q_norm).unwrap(),
}),
}),
&mut [
mlua::Value::Boolean(hits),
mlua::Value::Integer(distance),
mlua::Value::Vector(p_pos),
mlua::Value::Vector(p_norm),
mlua::Value::Vector(q_pos),
mlua::Value::Vector(q_norm),
]=>Ok(Self{
hits,
details:Some(Details{
distance:distance.into(),
p_pos:p64v3(p_pos).unwrap(),
p_norm:p64v3(p_norm).unwrap(),
q_pos:p64v3(q_pos).unwrap(),
q_norm:p64v3(q_norm).unwrap(),
}),
}),
values=>Err(mlua::Error::runtime(format!("Invalid return values: {values:?}"))),
}
}
}
struct Args{
query_p:Function,
radius_p:f64,
query_q:Function,
radius_q:f64,
test_intersection:bool,
}
impl Args{
fn new(
lua:&Lua,
mesh:&'static MinkowskiMesh<'static>,
point:Planar64Vec3,
test_intersection:bool,
)->LuaResult<Self>{
let radius_p=0.0;
let radius_q=0.0;
// Query the farthest point on the mesh in the given direction.
let query_p=lua.create_function(move|_,dir:Option<Vector>|{
let Some(dir)=dir else{
return Ok(vec(mesh.mesh0.hint_point()));
};
let dir=p64v3(dir).unwrap();
let vert_id=mesh.mesh0.farthest_vert(dir);
let dir=mesh.mesh0.vert(vert_id);
Ok(vec(dir))
})?;
// query_q is different since it includes the test point offset.
let query_q=lua.create_function(move|_,dir:Option<Vector>|{
let Some(dir)=dir else{
return Ok(vec(mesh.mesh1.hint_point()+point));
};
let dir=p64v3(dir).unwrap();
let vert_id=mesh.mesh1.farthest_vert(dir);
let dir=mesh.mesh1.vert(vert_id)+point;
Ok(vec(dir))
})?;
Ok(Args{
query_p,
radius_p,
query_q,
radius_q,
test_intersection,
})
}
}
impl IntoLuaMulti for Args{
fn into_lua_multi(self,lua:&Lua)->LuaResult<mlua::MultiValue>{
use mlua::IntoLua;
Ok(mlua::MultiValue::from_vec(vec![
self.query_p.into_lua(lua)?,
self.radius_p.into_lua(lua)?,
self.query_q.into_lua(lua)?,
self.radius_q.into_lua(lua)?,
mlua::Value::Nil,
self.test_intersection.into_lua(lua)?,
]))
}
}
fn minimum_difference(
mesh:&MinkowskiMesh,
point:Planar64Vec3,
test_intersection:bool,
)->LuaResult<MinimumDifference>{
let ctx=init_lua()?;
// SAFETY: mesh lifetime must outlive args usages
let mesh=unsafe{core::mem::transmute(mesh)};
let args=Args::new(&ctx.lua,mesh,point,test_intersection)?;
ctx.f.call(args)
}
struct Ctx{
lua:Lua,
f:Function,
}
fn init_lua()->LuaResult<Ctx>{
static SOURCE:std::sync::LazyLock<String>=std::sync::LazyLock::new(||std::fs::read_to_string("../../Trey-MinimumDifference.lua").unwrap());
let lua=Lua::new();
lua.sandbox(true)?;
let lib_f=lua.load(SOURCE.as_str()).set_name("Trey-MinimumDifference").into_function()?;
let lib:mlua::Table=lib_f.call(())?;
let f=lib.raw_get("difference")?;
Ok(Ctx{lua,f})
}

View File

@@ -80,8 +80,8 @@ pub enum MinkowskiFace{
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
pub mesh0:TransformedMesh<'a>,
pub mesh1:TransformedMesh<'a>,
}
// TODO: remove this
@@ -99,6 +99,9 @@ impl MinkowskiMesh<'_>{
mesh1,
}
}
pub fn closest_point(&self,point:Planar64Vec3)->Option<crate::mesh_query::FEV<Self>>{
crate::minimum_difference::closest_fev_not_inside(self,point)
}
pub fn predict_collision_in(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let start_position=match range.start_bound(){
Bound::Included(time)=>trajectory.extrapolated_position(*time),

View File

@@ -478,7 +478,7 @@ impl PhysicsMeshTransform{
#[derive(Debug,Clone,Copy)]
pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform,
pub transform:&'a PhysicsMeshTransform,
}
impl TransformedMesh<'_>{
pub const fn new<'a>(

View File

@@ -1003,6 +1003,12 @@ impl PhysicsData{
hitbox_mesh:StyleModifiers::default().calculate_mesh(),
}
}
pub fn closest_point(&self,mesh_id:u32,point:Planar64Vec3)->Option<crate::mesh_query::FEV<MinkowskiMesh<'_>>>{
let model_mesh=self.models.mesh(ConvexMeshId{model_id:PhysicsModelId::Contact(ContactModelId(mesh_id)),submesh_id:PhysicsSubmeshId::new(0)});
println!("transform={:?}",model_mesh.transform.vertex.matrix3);
let minkowski=MinkowskiMesh::minkowski_sum(model_mesh,self.hitbox_mesh.transformed_mesh());
minkowski.closest_point(point)
}
pub fn new(map:&map::CompleteMap)->Self{
let modes=map.modes.clone().denormalize();
let mut used_contact_attributes=Vec::new();

View File

@@ -76,3 +76,21 @@ fn physics_bug_3()->Result<(),ReplayError>{
Ok(())
}
#[test]
fn physics_bug_26()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692124338.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
println!("generating models..");
let physics_data=PhysicsData::new(&map);
println!("reproducing bug...");
//teleport to bug
let fev=physics_data.closest_point(1020,strafesnet_common::integer::vec3::try_from_f32_array([76.889,363.188,-309.263]).unwrap()).unwrap();
println!("{fev:?}");
Ok(())
}