Compare commits

..

22 Commits

Author SHA1 Message Date
6251c2d1b2 hack camera offset 2026-01-22 09:51:04 -08:00
21a90438f9 marble 2026-01-22 09:51:04 -08:00
edf1d8b699 bot file playback 2026-01-22 09:51:04 -08:00
fbd0f26958 add strafesnet_roblox_bot_file dep 2026-01-22 09:51:04 -08:00
848874d889 fix release build 2026-01-22 09:51:04 -08:00
6eae3bc350 optional directories 2026-01-22 09:51:04 -08:00
ae369085e3 toc 2026-01-22 09:51:04 -08:00
d86d7259d7 include map in binary 2026-01-22 09:51:04 -08:00
7843b414e7 use existing canvas 2026-01-22 09:51:04 -08:00
f9d4a8c344 use chrono instead of std::time 2026-01-22 09:51:04 -08:00
594684aea4 pick up wasm dep 2026-01-22 09:51:04 -08:00
fab7349b3e color functions 2026-01-22 09:51:04 -08:00
1c4a58de8b attach canvas 2026-01-22 09:51:04 -08:00
a310173182 replace enumerate_adapters with request_adapter 2026-01-22 09:51:04 -08:00
448b12f353 drop Send requirement 2026-01-22 09:51:04 -08:00
a4acb87a0d Initial trunk 2026-01-22 09:51:04 -08:00
2331bef281 pretty print time 2026-01-22 09:43:35 -08:00
31b52f7c34 Conditionally Advance Body in atomic_internal_instruction StrafeTick (#14)
Closes #13, but exposes the underlying issue at all times.

Reviewed-on: #14
Co-authored-by: Quaternions <krakow20@gmail.com>
Co-committed-by: Quaternions <krakow20@gmail.com>
2026-01-22 17:00:52 +00:00
b12c495a33 it: bug 3 2026-01-22 08:57:35 -08:00
8329eadb18 use unbounded range in physics tests 2026-01-22 08:47:54 -08:00
4b2f93be66 fix algorithm setup start position 2026-01-22 08:41:54 -08:00
469ab48156 allow unbounded range 2026-01-22 08:41:35 -08:00
21 changed files with 345 additions and 309 deletions

19
Cargo.lock generated
View File

@@ -3834,6 +3834,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
name = "strafe-client"
version = "0.11.0"
dependencies = [
"chrono",
"glam",
"parking_lot",
"pollster",
@@ -3843,9 +3844,13 @@ dependencies = [
"strafesnet_graphics",
"strafesnet_physics",
"strafesnet_rbx_loader",
"strafesnet_roblox_bot_file",
"strafesnet_session",
"strafesnet_settings",
"strafesnet_snf",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"wgpu",
"winit",
]
@@ -3904,7 +3909,6 @@ dependencies = [
"arrayvec",
"glam",
"id",
"mlua",
"strafesnet_common",
]
@@ -3927,6 +3931,17 @@ dependencies = [
"strafesnet_deferred_loader",
]
[[package]]
name = "strafesnet_roblox_bot_file"
version = "0.8.1"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "33d0fa524476d8b6cf23269b0c9cff6334b70585546b807cb8ec193858defecd"
dependencies = [
"binrw 0.15.0",
"bitflags 2.10.0",
"itertools 0.14.0",
]
[[package]]
name = "strafesnet_session"
version = "0.1.0"
@@ -4992,7 +5007,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]

View File

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

View File

@@ -3,7 +3,6 @@ mod face_crawler;
mod model;
mod push_solve;
mod minimum_difference;
mod minimum_difference_lua;
pub mod physics;

View File

@@ -10,7 +10,7 @@ use crate::model::{MinkowskiMesh,MinkowskiVert};
// written by Trey Reynolds in 2021
type Simplex<const N:usize,Vert>=[Vert;N];
#[derive(Clone,Copy,Debug)]
#[derive(Clone,Copy)]
enum Simplex1_3<Vert>{
Simplex1(Simplex<1,Vert>),
Simplex2(Simplex<2,Vert>),
@@ -136,15 +136,12 @@ 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;
@@ -168,7 +165,6 @@ fn reduce2<M:MeshQuery>(
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>{
println!("reduce2");
// --debug.profilebegin("reduceSimplex1")
// local a = a1 - a0
// local b = b1 - b0
@@ -226,7 +222,6 @@ fn reduce3<M:MeshQuery>(
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>{
println!("reduce3");
// --debug.profilebegin("reduceSimplex2")
// local a = a1 - a0
// local b = b1 - b0
@@ -336,7 +331,6 @@ fn reduce4<M:MeshQuery>(
mesh:&M,
point:Planar64Vec3,
)->Reduce<M::Vert>{
println!("reduce4");
// --debug.profilebegin("reduceSimplex3")
// local a = a1 - a0
// local b = b1 - b0
@@ -731,21 +725,11 @@ 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 is_intersecting={is_intersecting} simplex={simplex:?}");
if is_intersecting{
return None;
}
@@ -819,7 +803,6 @@ 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);
@@ -828,8 +811,6 @@ 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!("direction={direction}");
// new_point_p = queryP(-direction)
// new_point_q = queryQ(direction)
// local next_point = new_point_q - new_point_p
@@ -837,11 +818,7 @@ 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
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");
if ENABLE_FAST_FAIL&&direction.dot(next_pos+point).is_negative(){
return on_fast_fail();
}
@@ -850,11 +827,8 @@ 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
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");
if !direction.dot(next_pos-last_pos).is_positive()
||simplex_big.det_is_zero(mesh){
// Found enough information to compute the exact closest point.
// local norm = direction.unit
// local dist = a:Dot(norm)
@@ -867,7 +841,6 @@ 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);

View File

@@ -1,173 +0,0 @@
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("/home/quat/strafesnet/strafe-project/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

@@ -645,8 +645,8 @@ pub enum MinkowskiFace{
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
pub mesh0:TransformedMesh<'a>,
pub mesh1:TransformedMesh<'a>,
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
}
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
@@ -671,19 +671,24 @@ impl MinkowskiMesh<'_>{
}
}
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.position)?;
let start_position=match range.start_bound(){
Bound::Included(time)=>relative_body.extrapolated_position(*time),
Bound::Excluded(time)=>relative_body.extrapolated_position(*time),
Bound::Unbounded=>relative_body.position,
};
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
//continue forwards along the body parabola
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
}
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// TODO: handle unbounded collision using infinity fev
let time=match upper_bound{
Bound::Included(&time)=>time,
Bound::Excluded(&time)=>time,
Bound::Unbounded=>unimplemented!("unbounded collision out"),
let start_position=match upper_bound{
Bound::Included(time)=>relative_body.extrapolated_position(*time),
Bound::Excluded(time)=>relative_body.extrapolated_position(*time),
Bound::Unbounded=>relative_body.position,
};
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?;
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
// 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;

View File

@@ -28,6 +28,7 @@ pub enum InternalInstruction{
CollisionStart(Collision,model_physics::GigaTime),
CollisionEnd(Collision,model_physics::GigaTime),
StrafeTick,
// TODO: add GigaTime to ReachWalkTargetVelocity
ReachWalkTargetVelocity,
// Water,
}
@@ -261,6 +262,15 @@ pub struct PhysicsCamera{
impl PhysicsCamera{
const ANGLE_PITCH_LOWER_LIMIT:Angle32=Angle32::NEG_FRAC_PI_2;
const ANGLE_PITCH_UPPER_LIMIT:Angle32=Angle32::FRAC_PI_2;
pub fn new(
sensitivity:Ratio64Vec2,
clamped_mouse_pos:glam::IVec2,
)->Self{
Self{
sensitivity,
clamped_mouse_pos,
}
}
pub fn move_mouse(&mut self,mouse_delta:glam::IVec2){
let mut unclamped_mouse_pos=self.clamped_mouse_pos+mouse_delta;
unclamped_mouse_pos.y=unclamped_mouse_pos.y.clamp(
@@ -873,7 +883,7 @@ pub struct PhysicsState{
//input_state
input_state:InputState,
//style
style:StyleModifiers,//mode style with custom style updates applied
pub style:StyleModifiers,//mode style with custom style updates applied
//gameplay_state
mode_state:ModeState,
move_state:MoveState,
@@ -1669,7 +1679,20 @@ fn collision_start_intersect(
},
Some(gameplay_modes::Zone::Finish)=>{
match run.finish(time){
Ok(())=>println!("@@@@ Finished run time={}",run.time(time)),
Ok(())=>{
let time=run.time(time);
let h=time.get()/(Time::ONE_SECOND.get()*60*60);
let m=(time.get()/(Time::ONE_SECOND.get()*60)).rem_euclid(60);
let s=(time.get()/(Time::ONE_SECOND.get())).rem_euclid(60);
let ms=(time.get()/(Time::ONE_MILLISECOND.get())).rem_euclid(1000);
let us=(time.get()/(Time::ONE_MICROSECOND.get())).rem_euclid(1000);
let ns=(time.get()/(Time::ONE_NANOSECOND.get())).rem_euclid(1000);
if h==0{
println!("@@@@ Finished run time={m:02}:{s:02}.{ms:03}_{us:03}_{ns:03}");
}else{
println!("@@@@ Finished run time={h}:{m:02}:{s:02}.{ms:03}_{us:03}_{ns:03}");
}
},
Err(e)=>println!("@@@@ Run Finish error:{e:?}"),
}
},
@@ -1743,17 +1766,14 @@ fn collision_end_intersect(
}
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{
match ins.instruction{
// collisions advance the body precisely
InternalInstruction::CollisionStart(_,dt)
|InternalInstruction::CollisionEnd(_,dt)=>(true,Some(dt)),
InternalInstruction::StrafeTick
|InternalInstruction::ReachWalkTargetVelocity=>(true,None),
};
if should_advance_body{
match goober_time{
Some(dt)=>state.body.advance_time_ratio_dt(dt),
None=>state.body.advance_time(state.time),
}
|InternalInstruction::CollisionEnd(_,dt)=>state.body.advance_time_ratio_dt(dt),
// this advances imprecisely
InternalInstruction::ReachWalkTargetVelocity=>state.body.advance_time(state.time),
// strafe tick decides for itself whether to advance the body.
InternalInstruction::StrafeTick=>(),
}
match ins.instruction{
InternalInstruction::CollisionStart(collision,_)=>{
@@ -1800,6 +1820,8 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
let masked_controls=strafe_settings.mask(controls);
let control_dir=state.style.get_control_dir(masked_controls);
if control_dir!=vec3::zero(){
// manually advance time
state.body.advance_time(state.time);
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().wrap_1()){
//this is wrong but will work ig
@@ -1972,7 +1994,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,..);
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>){
@@ -1990,7 +2012,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,..);
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>){

View File

@@ -149,7 +149,7 @@ enum ViewState{
}
pub struct Session{
directories:Directories,
directories:Option<Directories>,
user_settings:UserSettings,
mouse_interpolator:MouseInterpolator,
view_state:ViewState,
@@ -164,7 +164,7 @@ pub struct Session{
impl Session{
pub fn new(
user_settings:UserSettings,
directories:Directories,
directories:Option<Directories>,
simulation:Simulation,
)->Self{
Self{
@@ -305,19 +305,21 @@ impl InstructionConsumer<Instruction<'_>> for Session{
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
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_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!");
});
if let Some(directories)=&self.directories{
let mut replays_path=directories.replays.clone();
let file_name=format!("{}.snfb",ins.time);
std::thread::spawn(move ||{
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!");
});
}
},
}
_=self.simulation.timer.set_paused(ins.time,false);

View File

@@ -60,8 +60,12 @@ fn physics_bug_3()->Result<(),ReplayError>{
// vec3::raw_xyz(0,-96915585363,1265),
// vec3::raw_xyz(0,-429496729600,0),
// corner setup before wall hits
vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
// vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
// vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
// vec3::raw_xyz(0,-429496729600,0),
// Actual bug 3 repro
vec3::raw_xyz(-2505538624455,3357963283914,557275711118),
vec3::raw_xyz(204188283920,-282280474198,166172785440),
vec3::raw_xyz(0,-429496729600,0),
Time::ZERO,
);

View File

@@ -16,6 +16,7 @@ source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies]
chrono = "0.4.39"
glam = "0.30.0"
parking_lot = "0.12.1"
pollster = "0.4.0"
@@ -25,9 +26,13 @@ strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "stra
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
strafesnet_roblox_bot_file = { version = "0.8.1", registry = "strafesnet" }
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
web-sys = { version = "0.3.76", features = ["console"] }
wgpu = "28.0.0"
winit = "0.30.7"

View File

@@ -4,12 +4,12 @@ use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::Time as SessionTime;
pub struct App<'a>{
root_time:std::time::Instant,
root_time:chrono::DateTime<chrono::Utc>,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>,
}
impl<'a> App<'a>{
pub fn new(
root_time:std::time::Instant,
root_time:chrono::DateTime<chrono::Utc>,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>,
)->App<'a>{
Self{
@@ -18,7 +18,7 @@ impl<'a> App<'a>{
}
}
fn send_timed_instruction(&mut self,instruction:Instruction){
let time=integer::Time::from_nanos(self.root_time.elapsed().as_nanos() as i64);
let time=integer::Time::from_nanos((chrono::Utc::now()-self.root_time).num_nanoseconds().unwrap());
self.window_thread.send(TimedInstruction{time,instruction}).unwrap();
}
}

View File

@@ -2,11 +2,11 @@ pub type QNWorker<'a,Task>=CompatNWorker<'a,Task>;
pub type INWorker<'a,Task>=CompatNWorker<'a,Task>;
pub struct CompatNWorker<'a,Task>{
f:Box<dyn FnMut(Task)+Send+'a>,
f:Box<dyn FnMut(Task)+'a>,
}
impl<'a,Task> CompatNWorker<'a,Task>{
pub fn new(f:impl FnMut(Task)+Send+'a)->CompatNWorker<'a,Task>{
pub fn new(f:impl FnMut(Task)+'a)->CompatNWorker<'a,Task>{
Self{
f:Box::new(f),
}

View File

@@ -87,10 +87,13 @@ pub enum LoadFormat{
Bot(strafesnet_snf::bot::Segment),
}
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
pub fn load_file<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
//blocking because it's simpler...
let file=std::fs::File::open(path).map_err(LoadError::File)?;
match read(file).map_err(LoadError::ReadError)?{
load(file)
}
pub fn load<R:Read+std::io::Seek>(reader:R)->Result<LoadFormat,LoadError>{
match read(reader).map_err(LoadError::ReadError)?{
#[cfg(feature="snf")]
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
#[cfg(feature="snf")]

View File

@@ -33,12 +33,12 @@ pub fn new(
},
Instruction::Resize(size,user_settings)=>{
println!("Resizing to {:?}",size);
let t0=std::time::Instant::now();
//let t0=std::time::Instant::now();
config.width=size.width.max(1);
config.height=size.height.max(1);
surface.configure(&device,&config);
graphics.resize(&device,&config,&user_settings);
println!("Resize took {:?}",t0.elapsed());
//println!("Resize took {:?}",t0.elapsed());
}
Instruction::Render(frame_state)=>{
//this has to go deeper somehow

View File

@@ -9,5 +9,8 @@ mod graphics_worker;
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));
fn main(){
setup::setup_and_start(TITLE);
#[cfg(target_arch="wasm32")]
wasm_bindgen_futures::spawn_local(setup::setup_and_start(TITLE));
#[cfg(not(target_arch="wasm32"))]
pollster::block_on(setup::setup_and_start(TITLE));
}

View File

@@ -1,13 +1,15 @@
use crate::graphics_worker::Instruction as GraphicsInstruction;
use strafesnet_settings::{directories::Directories,settings};
use strafesnet_session::session::{
FrameState,
Session,Simulation,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction,ImplicitModeInstruction,
Instruction as SessionInstruction,
};
use strafesnet_common::instruction::{TimedInstruction,InstructionConsumer};
use strafesnet_common::physics::Time as PhysicsTime;
use strafesnet_common::session::Time as SessionTime;
use strafesnet_common::timer::Timer;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::physics::{Time as PhysicsTime,TimeInner as PhysicsTimeInner};
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::timer::{Scaled,Timer,TimerState};
use strafesnet_physics::physics::PhysicsData;
pub enum Instruction{
SessionInput(SessionInputInstruction),
@@ -19,12 +21,136 @@ pub enum Instruction{
LoadReplay(strafesnet_snf::bot::Segment),
}
fn vector3_to_glam(v:&strafesnet_roblox_bot_file::v0::Vector3)->glam::Vec3{
glam::vec3(v.x,v.y,v.z)
}
fn f32_to_p64(f:f32)->strafesnet_common::integer::Planar64{
f.try_into().unwrap_or(strafesnet_common::integer::Planar64::ZERO)
}
struct PlayBacker{
//Instructions
timelines:strafesnet_roblox_bot_file::v0::Block,
//"Simulation"
event_id:usize,
offset:f64,
duration:f64,
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics_data:PhysicsData,
camera_offset:strafesnet_common::integer::Planar64Vec3,
}
impl PlayBacker{
pub fn new(
physics_data:PhysicsData,
timelines:strafesnet_roblox_bot_file::v0::Block,
camera_offset:strafesnet_common::integer::Planar64Vec3,
)->Self{
let first=timelines.output_events.first().unwrap();
let last=timelines.output_events.last().unwrap();
Self{
offset:first.time,
duration:last.time-first.time,
timelines,
event_id:0,
timer:Timer::from_state(Scaled::identity(),false),
physics_data,
camera_offset,
}
}
pub fn handle_instruction(&mut self,TimedInstruction{time,instruction}:&TimedInstruction<SessionInstruction,SessionTime>){
//match the instruction so the playback is pausable :D
match instruction{
&SessionInstruction::Control(SessionControlInstruction::SetPaused(paused))=>{
let _=self.timer.set_paused(*time,paused);
},
_=>(),
}
let simulation_time=self.timer.time(*time);
let mut time_float=simulation_time.get() as f64/PhysicsTime::ONE_SECOND.get() as f64+self.offset;
loop{
match self.timelines.output_events.get(self.event_id+1){
Some(next_event)=>{
if next_event.time<time_float{
self.event_id+=1;
}else{
break;
}
},
None=>{
//reset playback
self.event_id=0;
self.offset-=self.duration;
time_float-=self.duration;
},
}
}
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
use strafesnet_physics::physics::{Body,PhysicsCamera};
let time=self.timer.time(time);
let event0=&self.timelines.output_events[self.event_id];
let event1=&self.timelines.output_events[self.event_id+1];
let p0=vector3_to_glam(&event0.event.position);
let p1=vector3_to_glam(&event1.event.position);
let v0=vector3_to_glam(&event0.event.velocity);
let v1=vector3_to_glam(&event1.event.velocity);
let a0=vector3_to_glam(&event0.event.acceleration);
let a1=vector3_to_glam(&event1.event.acceleration);
let t0=event0.time;
let t1=event1.time;
let time_float=time.get() as f64/PhysicsTime::ONE_SECOND.get() as f64;
let t=((time_float+self.offset-t0)/(t1-t0)) as f32;
let p=p0.lerp(p1,t).to_array().map(f32_to_p64);
let v=v0.lerp(v1,t).to_array().map(f32_to_p64);
let a=a0.lerp(a1,t).to_array().map(f32_to_p64);
//println!("position={:?}",p);
let angles0=vector3_to_glam(&event0.event.angles);
let angles1=vector3_to_glam(&event1.event.angles);
let angles=angles0.lerp(angles1,t);
// mask mantissa out and set it to minimum value
// let ax_epsilon=f32::from_bits(angles.x.to_bits()&!((1<<23)-1)|1);
// let ay_epsilon=f32::from_bits(angles.y.to_bits()&!((1<<23)-1)|1);
let body=Body{
time,
position:strafesnet_common::integer::Planar64Vec3::new(p)+self.camera_offset,
velocity:strafesnet_common::integer::Planar64Vec3::new(v),
acceleration:strafesnet_common::integer::Planar64Vec3::new(a),
};
const FLOAT64_TO_ANGLE32_RADIANS:f64=((1i64<<31) as f64)/std::f64::consts::PI;
// xy is reversed in strafe client for some reason
let (ax,ay)=(
-angles.y as f64*FLOAT64_TO_ANGLE32_RADIANS,
-angles.x as f64*FLOAT64_TO_ANGLE32_RADIANS,
);
let camera=PhysicsCamera::new(
strafesnet_common::integer::Ratio64Vec2::new(1.0f32.try_into().unwrap(),1.0f32.try_into().unwrap()),
glam::ivec2(ax as i64 as i32,ay as i64 as i32)
);
FrameState{
body,
camera,
time,
}
}
pub fn user_settings(&self)->settings::UserSettings{
//oof, settings ignored
settings::UserSettings::default()
}
pub fn change_map(&mut self,_time:SessionTime,map:&strafesnet_common::map::CompleteMap){
self.physics_data=PhysicsData::new(&map);
}
}
pub fn new<'a>(
mut graphics_worker:crate::compat_worker::INWorker<'a,crate::graphics_worker::Instruction>,
directories:Directories,
directories:Option<Directories>,
user_settings:settings::UserSettings,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>{
let physics=strafesnet_physics::physics::PhysicsState::default();
let camera_offset=physics.style.camera_offset;
let timer=Timer::unpaused(SessionTime::ZERO,PhysicsTime::ZERO);
let simulation=Simulation::new(timer,physics);
let mut session=Session::new(
@@ -32,11 +158,20 @@ pub fn new<'a>(
directories,
simulation,
);
//load bot
let physics=PhysicsData::empty();
let data=include_bytes!("/home/quat/strafesnet/roblox_bot_file/files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d");
let bot_file=strafesnet_roblox_bot_file::v0::read_all_to_block(std::io::Cursor::new(data)).unwrap();
let mut interpolator=PlayBacker::new(
physics,
bot_file,
camera_offset,
);
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction,SessionTime>|{
// excruciating pain
macro_rules! run_session_instruction{
($time:expr,$instruction:expr)=>{
session.process_instruction(TimedInstruction{
interpolator.handle_instruction(&TimedInstruction{
time:$time,
instruction:$instruction,
});
@@ -59,16 +194,17 @@ pub fn new<'a>(
},
Instruction::Render=>{
run_session_instruction!(ins.time,SessionInstruction::Idle);
if let Some(frame_state)=session.get_frame_state(ins.time){
if let Some(frame_state)=Some(interpolator.get_frame_state(ins.time)){
run_graphics_worker_instruction!(GraphicsInstruction::Render(frame_state));
}
},
Instruction::Resize(physical_size)=>{
run_session_instruction!(ins.time,SessionInstruction::Idle);
let user_settings=session.user_settings().clone();
let user_settings=interpolator.user_settings().clone();
run_graphics_worker_instruction!(GraphicsInstruction::Resize(physical_size,user_settings));
},
Instruction::ChangeMap(complete_map)=>{
interpolator.change_map(ins.time,&complete_map);
run_session_instruction!(ins.time,SessionInstruction::ChangeMap(&complete_map));
run_session_instruction!(ins.time,SessionInstruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId::MAIN,strafesnet_common::gameplay_modes::StageId::FIRST))));
run_graphics_worker_instruction!(GraphicsInstruction::ChangeMap(complete_map));

View File

@@ -20,6 +20,16 @@ struct SetupContextPartial1{
fn create_window(title:&str,event_loop:&winit::event_loop::EventLoop<()>)->Result<winit::window::Window,winit::error::OsError>{
let mut attr=winit::window::WindowAttributes::default();
attr=attr.with_title(title);
#[cfg(target_arch="wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowAttributesExtWebSys;
let canvas=web_sys::window().unwrap()
.document().unwrap()
.get_element_by_id("canvas").unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
attr=attr.with_canvas(Some(canvas));
}
event_loop.create_window(attr)
}
fn create_instance()->SetupContextPartial1{
@@ -44,43 +54,20 @@ struct SetupContextPartial2<'a>{
surface:wgpu::Surface<'a>,
}
impl<'a> SetupContextPartial2<'a>{
fn pick_adapter(self)->SetupContextPartial3<'a>{
async fn pick_adapter(self)->SetupContextPartial3<'a>{
let adapter;
//TODO: prefer adapter that implements optional features
//let optional_features=optional_features();
let required_features=required_features();
//no helper function smh gotta write it myself
let adapters=pollster::block_on(self.instance.enumerate_adapters(self.backends));
let mut chosen_adapter=None;
let mut chosen_adapter_score=0;
for adapter in adapters {
if !adapter.is_surface_supported(&self.surface) {
continue;
}
let score=match adapter.get_info().device_type{
wgpu::DeviceType::IntegratedGpu=>3,
wgpu::DeviceType::DiscreteGpu=>4,
wgpu::DeviceType::VirtualGpu=>2,
wgpu::DeviceType::Other|wgpu::DeviceType::Cpu=>1,
};
let adapter_features=adapter.features();
if chosen_adapter_score<score&&adapter_features.contains(required_features) {
chosen_adapter_score=score;
chosen_adapter=Some(adapter);
}
}
if let Some(maybe_chosen_adapter)=chosen_adapter{
adapter=maybe_chosen_adapter;
}else{
panic!("No suitable GPU adapters found on the system!");
}
let chosen_adapter=self.instance.request_adapter(&wgpu::RequestAdapterOptions{
power_preference:wgpu::PowerPreference::HighPerformance,
force_fallback_adapter:false,
compatible_surface:Some(&self.surface),
}).await;
adapter=chosen_adapter.unwrap();
let adapter_info=adapter.get_info();
println!("Using {} ({:?})", adapter_info.name, adapter_info.backend);
@@ -110,14 +97,15 @@ struct SetupContextPartial3<'a>{
adapter:wgpu::Adapter,
}
impl<'a> SetupContextPartial3<'a>{
fn request_device(self)->SetupContextPartial4<'a>{
async fn request_device(self)->SetupContextPartial4<'a>{
let optional_features=optional_features();
let required_features=required_features();
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
let needed_limits=strafesnet_graphics::graphics::required_limits().using_resolution(self.adapter.limits());
let (device, queue)=pollster::block_on(self.adapter
let trace_dir=std::env::var("WGPU_TRACE");
let (device, queue)=self.adapter
.request_device(
&wgpu::DeviceDescriptor{
label:None,
@@ -127,7 +115,7 @@ impl<'a> SetupContextPartial3<'a>{
trace:wgpu::Trace::Off,
experimental_features:wgpu::ExperimentalFeatures::disabled(),
},
))
).await
.expect("Unable to find a suitable GPU adapter!");
SetupContextPartial4{
@@ -169,7 +157,7 @@ pub struct SetupContext<'a>{
pub config:wgpu::SurfaceConfiguration,
}
pub fn setup_and_start(title:&str){
pub async fn setup_and_start(title:&str){
let event_loop=winit::event_loop::EventLoop::new().unwrap();
println!("Initializing the surface...");
@@ -180,9 +168,9 @@ pub fn setup_and_start(title:&str){
let partial_2=partial_1.create_surface(&window).unwrap();
let partial_3=partial_2.pick_adapter();
let partial_3=partial_2.pick_adapter().await;
let partial_4=partial_3.request_device();
let partial_4=partial_3.request_device().await;
let size=window.inner_size();
@@ -196,16 +184,16 @@ pub fn setup_and_start(title:&str){
setup_context,
);
for arg in std::env::args().skip(1){
//for arg in std::env::args().skip(1){
window_thread.send(strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::ZERO,
instruction:crate::window::Instruction::WindowEvent(winit::event::WindowEvent::DroppedFile(arg.into())),
instruction:crate::window::Instruction::WindowEvent(winit::event::WindowEvent::DroppedFile("".into())),
}).unwrap();
};
//};
println!("Entering event loop...");
let mut app=crate::app::App::new(
std::time::Instant::now(),
chrono::Utc::now(),
window_thread
);
event_loop.run_app(&mut app).unwrap();

View File

@@ -57,7 +57,8 @@ impl WindowContext<'_>{
fn window_event(&mut self,time:SessionTime,event:winit::event::WindowEvent){
match event{
winit::event::WindowEvent::DroppedFile(path)=>{
match crate::file::load(path.as_path()){
let data=include_bytes!("/run/media/quat/Files/Documents/map-files/strafesnet/maps/bhop_snfm/5692093612.snfm");
match crate::file::load(std::io::Cursor::new(data)){
Ok(LoadFormat::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
Ok(LoadFormat::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}).unwrap(),
Err(e)=>println!("Failed to load file: {e}"),
@@ -236,17 +237,21 @@ impl WindowContext<'_>{
}
}
}
pub fn worker<'a>(
window:&'a winit::window::Window,
setup_context:crate::setup::SetupContext<'a>,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>{
// WindowContextSetup::new
#[cfg(feature="user-install")]
let directories=Directories::user().unwrap();
let directories=Directories::user();
#[cfg(not(feature="user-install"))]
let directories=Directories::portable().unwrap();
let directories=Directories::portable().ok();
let user_settings=directories.settings();
let user_settings=match &directories{
Some(directories)=>directories.settings(),
None=>strafesnet_settings::settings::UserSettings::default(),
};
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
graphics.load_user_settings(&user_settings);

1
web-client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/dist

2
web-client/Trunk.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
target = "index.html"

47
web-client/index.html Normal file
View File

@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, height=device-height, initial-scale=1"
/>
<title>Strafe Client</title>
<base data-trunk-public-url />
<style type="text/css">
:focus {
outline: none;
}
body {
margin: 0px;
background: #fff;
width: 100%;
height: 100%;
}
#content {
/* This allows the flexbox to grow to max size, this is needed for WebGPU */
flex: 1 1 100%;
/* This forces CSS to ignore the width/height of the canvas, this is needed for WebGL */
contain: size;
}
.main-canvas {
margin: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas class="main-canvas" id="canvas"></canvas>
<link
data-trunk
rel="rust"
href="../strafe-client/Cargo.toml"
data-wasm-opt-params="--enable-bulk-memory --enable-nontrapping-float-to-int"
/>
</body>
</html>