Compare commits
14 Commits
mesh-shade
...
luau-md-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
2ca197431b
|
|||
|
13f93fb2f0
|
|||
|
ee72434a87
|
|||
|
8c783ed8ae
|
|||
|
d90c201a91
|
|||
|
2c6a373fd0
|
|||
|
733bfc1bbf
|
|||
|
695ed460fc
|
|||
|
9b308e5fb7
|
|||
|
971ecc6393
|
|||
|
89c86cc805
|
|||
|
4f09bb41db
|
|||
|
dfe0987b84
|
|||
|
493cf242c6
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3904,6 +3904,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"glam",
|
||||
"id",
|
||||
"mlua",
|
||||
"strafesnet_common",
|
||||
]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2024"
|
||||
arrayvec = "0.7.6"
|
||||
glam = "0.30.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
mlua = { version = "0.11.5", features = ["luau"] }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -3,6 +3,7 @@ mod face_crawler;
|
||||
mod model;
|
||||
mod push_solve;
|
||||
mod minimum_difference;
|
||||
mod minimum_difference_lua;
|
||||
|
||||
pub mod physics;
|
||||
|
||||
|
||||
@@ -10,13 +10,20 @@ use crate::model::{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>),
|
||||
Simplex3(Simplex<3,Vert>),
|
||||
}
|
||||
impl<Vert> Simplex1_3<Vert>{
|
||||
fn len(&self)->usize{
|
||||
match self{
|
||||
Simplex1_3::Simplex1(_)=>1,
|
||||
Simplex1_3::Simplex2(_)=>2,
|
||||
Simplex1_3::Simplex3(_)=>3,
|
||||
}
|
||||
}
|
||||
fn push_front(self,v:Vert)->Simplex2_4<Vert>{
|
||||
match self{
|
||||
Simplex1_3::Simplex1([v0])=>Simplex2_4::Simplex2([v,v0]),
|
||||
@@ -136,12 +143,15 @@ fn reduce1<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>{
|
||||
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;
|
||||
@@ -165,6 +175,7 @@ fn reduce2<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>{
|
||||
println!("reduce2");
|
||||
// --debug.profilebegin("reduceSimplex1")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
@@ -222,6 +233,7 @@ fn reduce3<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>{
|
||||
println!("reduce3");
|
||||
// --debug.profilebegin("reduceSimplex2")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
@@ -236,6 +248,7 @@ fn reduce3<M:MeshQuery>(
|
||||
let p=-(p0+point);
|
||||
let mut u=p1-p0;
|
||||
let v=p2-p0;
|
||||
println!("p={p} u={u} v={v}");
|
||||
|
||||
// local uv = u:Cross(v)
|
||||
// local up = u:Cross(p)
|
||||
@@ -259,6 +272,7 @@ fn reduce3<M:MeshQuery>(
|
||||
}else{
|
||||
uv
|
||||
};
|
||||
println!("and we got here?? direction={direction:?}");
|
||||
|
||||
// return direction, a0, a1, b0, b1, c0, c1
|
||||
return Reduced{
|
||||
@@ -331,6 +345,7 @@ fn reduce4<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduce<M::Vert>{
|
||||
println!("reduce4");
|
||||
// --debug.profilebegin("reduceSimplex3")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
@@ -366,9 +381,11 @@ fn reduce4<M:MeshQuery>(
|
||||
let uv_p=uv.dot(p);
|
||||
|
||||
// if pvw/uvw >= 0 and upw/uvw >= 0 and uvp/uvw >= 0 then
|
||||
if !pv_w.div_sign(uv_w).is_negative()
|
||||
||!up_w.div_sign(uv_w).is_negative()
|
||||
||!uv_p.div_sign(uv_w).is_negative(){
|
||||
let a=!pv_w.div_sign(uv_w).is_negative();
|
||||
let b=!up_w.div_sign(uv_w).is_negative();
|
||||
let c=!uv_p.div_sign(uv_w).is_negative();
|
||||
println!("a={a} b={b} c={c}");
|
||||
if a&&b&&c{
|
||||
// origin is contained, this is a positive detection
|
||||
// local direction = Vector3.new(0, 0, 0)
|
||||
// return direction, a0, a1, b0, b1, c0, c1, d0, d1
|
||||
@@ -751,13 +768,22 @@ 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
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -821,6 +847,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
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);
|
||||
@@ -829,6 +856,8 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
// exitRadius = testIntersection and 0 or exitRadius or 1/0
|
||||
// for _ = 1, 100 do
|
||||
loop{
|
||||
println!("arity={} direction={direction}",simplex_small.len());
|
||||
|
||||
// new_point_p = queryP(-direction)
|
||||
// new_point_q = queryQ(direction)
|
||||
// local next_point = new_point_q - new_point_p
|
||||
@@ -836,7 +865,11 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -845,8 +878,11 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
// 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 cond1=!direction.dot(next_pos-last_pos).is_positive();
|
||||
let cond2=simplex_big.det_is_zero(mesh);
|
||||
println!("cond1={cond1} cond2={cond2}");
|
||||
if cond1||cond2{
|
||||
println!("on_exact");
|
||||
// Found enough information to compute the exact closest point.
|
||||
// local norm = direction.unit
|
||||
// local dist = a:Dot(norm)
|
||||
@@ -859,6 +895,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
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);
|
||||
|
||||
173
engine/physics/src/minimum_difference_lua.rs
Normal file
173
engine/physics/src/minimum_difference_lua.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use mlua::{Lua,FromLuaMulti,IntoLuaMulti,Function,Result as LuaResult,Vector};
|
||||
use strafesnet_common::integer::{Planar64,Planar64Vec3,FixedFromFloatError};
|
||||
|
||||
use crate::model::{MeshQuery,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})
|
||||
}
|
||||
@@ -91,7 +91,6 @@ pub trait MeshQuery{
|
||||
(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;
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->Self::Vert;
|
||||
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
|
||||
@@ -646,8 +645,8 @@ pub enum MinkowskiFace{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MinkowskiMesh<'a>{
|
||||
mesh0:TransformedMesh<'a>,
|
||||
mesh1:TransformedMesh<'a>,
|
||||
pub mesh0:TransformedMesh<'a>,
|
||||
pub mesh1:TransformedMesh<'a>,
|
||||
}
|
||||
|
||||
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
|
||||
|
||||
@@ -115,3 +115,48 @@ fn bug_3(){
|
||||
assert_eq!(body.acceleration,vec3::int(0,0,0));
|
||||
assert_eq!(body.time,Time::from_secs(2));
|
||||
}
|
||||
|
||||
fn test_scene_cylinder()->PhysicsData{
|
||||
let mut builder=TestSceneBuilder::new();
|
||||
let cube_face_description=CubeFaceDescription::new(Default::default(),RenderConfigId::new(0));
|
||||
let mesh=builder.push_mesh(strafesnet_rbx_loader::primitives::unit_cylinder(cube_face_description));
|
||||
// place one 5x5x5 cylinder.
|
||||
// log cylinder
|
||||
// transform=PhysicsMeshTransform { vertex: Planar64Affine3 { matrix3: Matrix { array: [[-0.00000469, 20.41880000, -0.00001730], [-3.71969533, -0.00000d38, -11.c3c43095], [-11.c3c41e7d, 0.00000000, 3.719690ac]] }, translation: Vector { array: [-908.de600000, 28f.e5240000, 24c.c4d80000] } }, normal: Matrix { array: [[-0.00002d857e4861a0, 147.71af629e398c1d05, -0.0000ead3c8730458], [-6f.147da83887600000, -0.0001ab1b4486f8fc, -23d.04a89b8def680000], [-23d.04aae32fa55a8280, 0.0000018177694d73, 6f.147e3a4136524bf8]] }, det: 2942.07be40fdd83c96df87d34320 }
|
||||
// wedge part
|
||||
// transform=PhysicsMeshTransform { vertex: Planar64Affine3 { matrix3: Matrix { array: [[-4.9ba5b000, 0.00000000, 0.00000000], [0.00000000, 3.85509c00, 0.00000000], [0.00000000, 0.00000000, -4.9ba5b000]] }, translation: Vector { array: [-781.30380000, 35b.dab40000, 11c.a4160000] } }, normal: Matrix { array: [[-10.3941970ff7400000, 0.0000000000000000, 0.0000000000000000], [0.0000000000000000, 15.3bcf8e5c59000000, 0.0000000000000000], [0.0000000000000000, 0.0000000000000000, -10.3941970ff7400000]] }, det: 4a.c2312159fcd9163c00000000 }
|
||||
builder.push_mesh_instance(mesh,Planar64Affine3::new(
|
||||
mat3::Matrix3::from_cols([vec3::raw_array([-0x4_9ba5b000, 0x0_00000000, 0x0_00000000]), vec3::raw_array([0x0_00000000, 0x3_85509c00, 0x0_00000000]), vec3::raw_array([0x0_00000000, 0x0_00000000, -0x4_9ba5b000])]),
|
||||
vec3::raw_array([-0x781_30380000, 0x35b_dab40000, 0x11c_a4160000])
|
||||
));
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimum_difference(){
|
||||
let physics_data=test_scene_cylinder();
|
||||
// log cylinder
|
||||
// H p(-2329.436, 694.175, 587.190) v(0.000, -5.714, 0.000) a(0.000, -100.000, 0.000) t(56s+880000000ns)
|
||||
// wedge part
|
||||
// H p(-1920.041, 867.905, 284.639) v(0.000, -4.695, 0.000) a(0.000, -100.000, 0.000) t(119s+260000000ns)
|
||||
let body=strafesnet_physics::physics::Body::new(
|
||||
vec3::try_from_f32_array([-1920.041, 867.905, 284.639]).unwrap(),
|
||||
vec3::try_from_f32_array([0.000, -4.695, 0.000]).unwrap(),
|
||||
vec3::int(0,-100,0),
|
||||
Time::ZERO,
|
||||
);
|
||||
let mut physics=PhysicsState::new_with_body(body);
|
||||
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
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
|
||||
// touch top of part at 5,-5,0
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(2));
|
||||
assert!(phys_iter.next().is_none());
|
||||
let body=physics.body();
|
||||
assert_eq!(body.position,vec3::int(5+2,0,0)>>1);
|
||||
assert_eq!(body.velocity,vec3::int(0,0,0));
|
||||
assert_eq!(body.acceleration,vec3::int(0,0,0));
|
||||
assert_eq!(body.time,Time::from_secs(2));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user