Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
b3d3900628 | |||
2c11e2ef8e | |||
d732ae7ab3 | |||
9d5c5c1548 | |||
e5c1b85edd | |||
56fc7ed1bf | |||
937d0f12ba | |||
2a7dbbe3e4 | |||
79a22ae303 | |||
6dbef74faa | |||
a44132840e | |||
808593ecbe | |||
bc59d65d15 |
@ -1,2 +1,6 @@
|
||||
[registries.strafesnet]
|
||||
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2251,7 +2251,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strafe-client"
|
||||
version = "0.11.0"
|
||||
version = "0.10.5"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
|
@ -62,7 +62,6 @@ impl<I,T> InstructionCollector<I,T>
|
||||
pub const fn time(&self)->Time<T>{
|
||||
self.time
|
||||
}
|
||||
#[inline]
|
||||
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
|
||||
if let Some(ins)=instruction{
|
||||
if ins.time<self.time{
|
||||
@ -71,8 +70,7 @@ impl<I,T> InstructionCollector<I,T>
|
||||
}
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn take(self)->Option<TimedInstruction<I,T>>{
|
||||
pub fn instruction(self)->Option<TimedInstruction<I,T>>{
|
||||
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
|
||||
self.instruction.map(|instruction|TimedInstruction{
|
||||
time:self.time,
|
||||
|
@ -14,7 +14,6 @@ impl<T> Time<T>{
|
||||
pub const MIN:Self=Self::raw(i64::MIN);
|
||||
pub const MAX:Self=Self::raw(i64::MAX);
|
||||
pub const ZERO:Self=Self::raw(0);
|
||||
pub const EPSILON:Self=Self::raw(1);
|
||||
pub const ONE_SECOND:Self=Self::raw(1_000_000_000);
|
||||
pub const ONE_MILLISECOND:Self=Self::raw(1_000_000);
|
||||
pub const ONE_MICROSECOND:Self=Self::raw(1_000);
|
||||
|
@ -76,7 +76,7 @@ impl Run{
|
||||
match &self.state{
|
||||
RunState::Created=>Time::ZERO,
|
||||
RunState::Started{timer}=>timer.time(time),
|
||||
RunState::Finished{timer}=>timer.time(),
|
||||
RunState::Finished{timer}=>timer.time(time),
|
||||
}
|
||||
}
|
||||
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
@ -110,10 +110,4 @@ impl Run{
|
||||
self.flagged=Some(flag_reason);
|
||||
}
|
||||
}
|
||||
pub fn get_finish_time(&self)->Option<Time>{
|
||||
match &self.state{
|
||||
RunState::Finished{timer}=>Some(timer.time()),
|
||||
_=>None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ impl<T:TimerState> TimerFixed<T,Paused>
|
||||
where Time<T::In>:Copy,
|
||||
{
|
||||
pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{
|
||||
let new_time=self.time();
|
||||
let new_time=self.time(time);
|
||||
let mut timer=TimerFixed{
|
||||
state:self.state,
|
||||
_paused:Unpaused,
|
||||
@ -165,9 +165,6 @@ impl<T:TimerState> TimerFixed<T,Paused>
|
||||
timer.set_time(time,new_time);
|
||||
timer
|
||||
}
|
||||
pub fn time(&self)->Time<T::Out>{
|
||||
self.state.get_offset().coerce()
|
||||
}
|
||||
}
|
||||
impl<T:TimerState> TimerFixed<T,Unpaused>
|
||||
where Time<T::In>:Copy,
|
||||
@ -181,9 +178,6 @@ impl<T:TimerState> TimerFixed<T,Unpaused>
|
||||
timer.set_time(time,new_time);
|
||||
timer
|
||||
}
|
||||
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
|
||||
self.state.get_time(time)
|
||||
}
|
||||
}
|
||||
|
||||
//the new constructor and time queries are generic across both
|
||||
@ -205,6 +199,12 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
|
||||
pub fn into_state(self)->T{
|
||||
self.state
|
||||
}
|
||||
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
|
||||
match P::IS_PAUSED{
|
||||
true=>self.state.get_offset().coerce(),
|
||||
false=>self.state.get_time(time),
|
||||
}
|
||||
}
|
||||
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
|
||||
match P::IS_PAUSED{
|
||||
true=>self.state.set_offset(new_time.coerce()),
|
||||
@ -256,7 +256,7 @@ impl<T:TimerState> Timer<T>
|
||||
}
|
||||
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
|
||||
match self{
|
||||
Self::Paused(timer)=>timer.time(),
|
||||
Self::Paused(timer)=>timer.time(time),
|
||||
Self::Unpaused(timer)=>timer.time(time),
|
||||
}
|
||||
}
|
||||
@ -329,7 +329,7 @@ mod test{
|
||||
//create a paused timer that reads 0s
|
||||
let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0)));
|
||||
//the paused timer at 1 second should read 0s
|
||||
assert_eq!(timer.time(),sec!(0));
|
||||
assert_eq!(timer.time(sec!(1)),sec!(0));
|
||||
|
||||
//unpause it after one second
|
||||
let timer=timer.into_unpaused(sec!(1));
|
||||
@ -339,7 +339,7 @@ mod test{
|
||||
//pause the timer after 11 seconds
|
||||
let timer=timer.into_paused(sec!(11));
|
||||
//the paused timer at 20 seconds should read 5s
|
||||
assert_eq!(timer.time(),sec!(5));
|
||||
assert_eq!(timer.time(sec!(20)),sec!(5));
|
||||
}
|
||||
#[test]
|
||||
fn test_timer()->Result<(),Error>{
|
||||
|
@ -1,22 +1,22 @@
|
||||
macro_rules! type_from_lua_userdata{
|
||||
($ty:ident)=>{
|
||||
impl mlua::FromLua for $ty{
|
||||
($asd:ident)=>{
|
||||
impl mlua::FromLua for $asd{
|
||||
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
|
||||
match value{
|
||||
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! type_from_lua_userdata_lua_lifetime{
|
||||
($ty:ident)=>{
|
||||
impl mlua::FromLua for $ty<'static>{
|
||||
($asd:ident)=>{
|
||||
impl mlua::FromLua for $asd<'static>{
|
||||
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
|
||||
match value{
|
||||
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
|
||||
match delay.classify(){
|
||||
std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?,
|
||||
// cases where the number is too large to schedule
|
||||
std::num::FpCategory::Infinite
|
||||
|std::num::FpCategory::Normal if (u64::MAX as f64)<delay=>{
|
||||
std::num::FpCategory::Infinite=>return Ok(()),
|
||||
std::num::FpCategory::Normal=>if (u64::MAX as f64)<delay{
|
||||
return Ok(());
|
||||
},
|
||||
_=>(),
|
||||
|
@ -4,8 +4,6 @@ use crate::newtypes;
|
||||
use crate::file::BlockId;
|
||||
use strafesnet_common::physics::Time;
|
||||
|
||||
const VERSION:u32=0;
|
||||
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -30,16 +28,12 @@ pub enum Error{
|
||||
/* block types
|
||||
|
||||
BLOCK_BOT_HEADER:
|
||||
// Segments are laid out in chronological order,
|
||||
// Tegments are laid out in chronological order,
|
||||
// but block_id is not necessarily in ascending order.
|
||||
//
|
||||
// This is to place the final segment close to the start of the file,
|
||||
// which allows the duration of the bot to be conveniently calculated
|
||||
// from the first and last instruction timestamps.
|
||||
//
|
||||
// Use exact physics version for replay playback
|
||||
// Use highest compatible physics version for verification
|
||||
u32 physics_version
|
||||
u32 num_segments
|
||||
for _ in 0..num_segments{
|
||||
i64 time
|
||||
@ -67,7 +61,6 @@ struct SegmentHeader{
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct Header{
|
||||
physics_version:u32,
|
||||
num_segments:u32,
|
||||
#[br(count=num_segments)]
|
||||
segments:Vec<SegmentHeader>,
|
||||
@ -76,7 +69,7 @@ struct Header{
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
#[derive(Clone,Copy,Debug,id::Id)]
|
||||
pub struct SegmentId(u32);
|
||||
struct SegmentId(u32);
|
||||
|
||||
pub struct Segment{
|
||||
pub instructions:Vec<TimedPhysicsInstruction>
|
||||
@ -158,7 +151,7 @@ impl<R:BinReaderExt> StreamableBot<R>{
|
||||
}
|
||||
|
||||
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
|
||||
pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
|
||||
pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
|
||||
// decide which instructions to put in which segment
|
||||
// write segment 1 to block 1
|
||||
// write segment N to block 2
|
||||
@ -258,7 +251,6 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:i
|
||||
};
|
||||
|
||||
let header=Header{
|
||||
physics_version,
|
||||
num_segments:num_segments as u32,
|
||||
segments,
|
||||
};
|
||||
@ -311,7 +303,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:i
|
||||
|
||||
let file_header=crate::file::Header{
|
||||
fourcc:crate::file::FourCC::Bot,
|
||||
version:VERSION,
|
||||
version:0,
|
||||
priming,
|
||||
resource:0,
|
||||
block_count,
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafe-client"
|
||||
version = "0.11.0"
|
||||
version = "0.10.5"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "Custom"
|
||||
|
@ -12,20 +12,6 @@ pub enum CrawlResult<M:MeshQuery>{
|
||||
Miss(FEV<M>),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
impl<M:MeshQuery> CrawlResult<M>{
|
||||
pub fn hit(self)->Option<(M::Face,GigaTime)>{
|
||||
match self{
|
||||
CrawlResult::Miss(_)=>None,
|
||||
CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
}
|
||||
pub fn miss(self)->Option<FEV<M>>{
|
||||
match self{
|
||||
CrawlResult::Miss(fev)=>Some(fev),
|
||||
CrawlResult::Hit(_,_)=>None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
where
|
||||
|
@ -22,7 +22,7 @@ impl std::fmt::Display for ReadError{
|
||||
}
|
||||
impl std::error::Error for ReadError{}
|
||||
|
||||
pub enum ReadFormat{
|
||||
enum Format{
|
||||
#[cfg(feature="roblox")]
|
||||
Roblox(strafesnet_rbx_loader::Model),
|
||||
#[cfg(feature="source")]
|
||||
@ -33,26 +33,22 @@ pub enum ReadFormat{
|
||||
SNFB(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
|
||||
pub fn read<R:Read+std::io::Seek>(input:R)->Result<Format,ReadError>{
|
||||
let mut buf=std::io::BufReader::new(input);
|
||||
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned();
|
||||
// reading the entire file is way faster than round tripping the disk constantly
|
||||
let mut entire_file=Vec::new();
|
||||
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
|
||||
let cursor=std::io::Cursor::new(entire_file);
|
||||
match peek.as_slice(){
|
||||
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
|
||||
match &peek[0..4]{
|
||||
#[cfg(feature="roblox")]
|
||||
b"<rob"=>Ok(ReadFormat::Roblox(strafesnet_rbx_loader::read(cursor).map_err(ReadError::Roblox)?)),
|
||||
b"<rob"=>Ok(Format::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)),
|
||||
#[cfg(feature="source")]
|
||||
b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)),
|
||||
b"VBSP"=>Ok(Format::Source(strafesnet_bsp_loader::read(buf).map_err(ReadError::Source)?)),
|
||||
#[cfg(feature="snf")]
|
||||
b"SNFM"=>Ok(ReadFormat::SNFM(
|
||||
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
|
||||
b"SNFM"=>Ok(Format::SNFM(
|
||||
strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)?
|
||||
.into_complete_map().map_err(ReadError::StrafesNETMap)?
|
||||
)),
|
||||
#[cfg(feature="snf")]
|
||||
b"SNFB"=>Ok(ReadFormat::SNFB(
|
||||
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
|
||||
b"SNFB"=>Ok(Format::SNFB(
|
||||
strafesnet_snf::read_bot(buf).map_err(ReadError::StrafesNET)?
|
||||
.read_all().map_err(ReadError::StrafesNETBot)?
|
||||
)),
|
||||
_=>Err(ReadError::UnknownFileFormat),
|
||||
@ -72,23 +68,23 @@ impl std::fmt::Display for LoadError{
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
|
||||
pub enum LoadFormat{
|
||||
pub enum Format2{
|
||||
#[cfg(feature="snf")]
|
||||
Map(strafesnet_common::map::CompleteMap),
|
||||
#[cfg(feature="snf")]
|
||||
Bot(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
|
||||
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<Format2,LoadError>{
|
||||
//blocking because it's simpler...
|
||||
let file=std::fs::File::open(path).map_err(LoadError::File)?;
|
||||
match read(file).map_err(LoadError::ReadError)?{
|
||||
#[cfg(feature="snf")]
|
||||
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
|
||||
Format::SNFB(bot)=>Ok(Format2::Bot(bot)),
|
||||
#[cfg(feature="snf")]
|
||||
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
|
||||
Format::SNFM(map)=>Ok(Format2::Map(map)),
|
||||
#[cfg(feature="roblox")]
|
||||
ReadFormat::Roblox(model)=>{
|
||||
Format::Roblox(model)=>{
|
||||
let mut place=model.into_place();
|
||||
place.run_scripts();
|
||||
|
||||
@ -121,10 +117,10 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
|
||||
)
|
||||
);
|
||||
|
||||
Ok(LoadFormat::Map(map))
|
||||
Ok(Format2::Map(map))
|
||||
},
|
||||
#[cfg(feature="source")]
|
||||
ReadFormat::Source(bsp)=>{
|
||||
Format::Source(bsp)=>{
|
||||
let mut loader=strafesnet_deferred_loader::source_legacy();
|
||||
|
||||
let (texture_loader,mesh_loader)=loader.get_inner_mut();
|
||||
@ -160,7 +156,7 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
|
||||
),
|
||||
);
|
||||
|
||||
Ok(LoadFormat::Map(map))
|
||||
Ok(Format2::Map(map))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -718,7 +718,7 @@ impl MinkowskiMesh<'_>{
|
||||
//
|
||||
// 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:Time,)->Option<FEV<MinkowskiMesh>>{
|
||||
fn closest_fev_not_inside(&self,mut infinity_body:Body)->Option<FEV<MinkowskiMesh>>{
|
||||
infinity_body.infinity_dir().map_or(None,|dir|{
|
||||
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
|
||||
//a line is simpler to solve than a parabola
|
||||
@ -726,32 +726,40 @@ impl MinkowskiMesh<'_>{
|
||||
infinity_body.acceleration=vec3::ZERO;
|
||||
//crawl in from negative infinity along a tangent line to get the closest fev
|
||||
// TODO: change crawl_fev args to delta time? Optional values?
|
||||
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
|
||||
match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){
|
||||
crate::face_crawler::CrawlResult::Miss(fev)=>Some(fev),
|
||||
crate::face_crawler::CrawlResult::Hit(_,_)=>None,
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone(),start_time).map_or(None,|fev|{
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone()).map_or(None,|fev|{
|
||||
//continue forwards along the body parabola
|
||||
fev.crawl(self,relative_body,start_time,time_limit).hit()
|
||||
match fev.crawl(self,relative_body,relative_body.time,time_limit){
|
||||
crate::face_crawler::CrawlResult::Miss(_)=>None,
|
||||
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
//create an extrapolated body at time_limit
|
||||
let infinity_body=-relative_body.clone();
|
||||
self.closest_fev_not_inside(infinity_body,-time_limit).map_or(None,|fev|{
|
||||
let infinity_body=Body::new(
|
||||
relative_body.extrapolated_position(time_limit),
|
||||
-relative_body.extrapolated_velocity(time_limit),
|
||||
relative_body.acceleration,
|
||||
-time_limit,
|
||||
);
|
||||
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
|
||||
//continue backwards along the body parabola
|
||||
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
|
||||
//no need to test -time<time_limit because of the first step
|
||||
.map(|(face,time)|(face,-time))
|
||||
match fev.crawl(self,&-relative_body.clone(),-time_limit,-relative_body.time){
|
||||
crate::face_crawler::CrawlResult::Miss(_)=>None,
|
||||
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,-time)),//no need to test -time<time_limit because of the first step
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_face_out(&self,relative_body:&Body,start_time:Time,time_limit:Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
|
||||
pub fn predict_collision_face_out(&self,relative_body:&Body,time_limit:Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
|
||||
//no algorithm needed, there is only one state and two cases (Edge,None)
|
||||
//determine when it passes an edge ("sliding off" case)
|
||||
let start_time={
|
||||
let r=(start_time-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num,r.den)
|
||||
};
|
||||
let mut best_time={
|
||||
let r=(time_limit-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
@ -767,7 +775,7 @@ impl MinkowskiMesh<'_>{
|
||||
//WARNING! d outside of *2
|
||||
//WARNING: truncated precision
|
||||
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){
|
||||
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
if Ratio::new(Planar64::ZERO,Planar64::EPSILON).le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_edge=Some(directed_edge_id);
|
||||
break;
|
||||
@ -778,7 +786,10 @@ impl MinkowskiMesh<'_>{
|
||||
}
|
||||
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
|
||||
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
|
||||
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
|
||||
match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){
|
||||
crate::face_crawler::CrawlResult::Miss(_)=>None,
|
||||
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
}
|
||||
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
|
||||
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
|
||||
@ -984,7 +995,7 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
|
||||
for k in 0..len{
|
||||
if k!=i&&k!=j{
|
||||
let d=n.dot(normals[k]).is_negative();
|
||||
if let &Some(comp)=&d_comp{
|
||||
if let Some(comp)=&d_comp{
|
||||
// This is testing if d_comp*d < 0
|
||||
if comp^d{
|
||||
return true;
|
||||
|
@ -14,45 +14,6 @@ use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Ma
|
||||
pub use strafesnet_common::physics::{Time,TimeInner};
|
||||
use gameplay::ModeState;
|
||||
|
||||
// Physics bug fixes can easily desync all bots.
|
||||
//
|
||||
// When replaying a bot, use the exact physics version which it was recorded with.
|
||||
//
|
||||
// When validating a new bot, ignore the version and use the latest version,
|
||||
// and overwrite the version in the file.
|
||||
//
|
||||
// Compatible physics versions should be determined
|
||||
// empirically at development time via leaderboard resimulation.
|
||||
//
|
||||
// Compatible physics versions should result in an identical leaderboard state,
|
||||
// or the only bots which fail are ones exploiting a surgically patched bug.
|
||||
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
|
||||
pub struct PhysicsVersion(u32);
|
||||
pub const VERSION:PhysicsVersion=PhysicsVersion(1);
|
||||
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
|
||||
let compat=[0,1];
|
||||
|
||||
let mut input_version=0;
|
||||
while input_version<compat.len(){
|
||||
// compatible version must be greater that or equal to the input version
|
||||
assert!(input_version as u32<=compat[input_version]);
|
||||
// compatible version must be a version that exists
|
||||
assert!(compat[input_version]<=VERSION.0);
|
||||
input_version+=1;
|
||||
}
|
||||
compat
|
||||
};
|
||||
pub enum PhysicsVersionError{
|
||||
UnknownPhysicsVersion,
|
||||
}
|
||||
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
|
||||
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
|
||||
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
|
||||
}else{
|
||||
Err(PhysicsVersionError::UnknownPhysicsVersion)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Body=crate::body::Body<TimeInner>;
|
||||
type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
|
||||
|
||||
@ -71,7 +32,7 @@ pub enum InternalInstruction{
|
||||
// Water,
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
#[derive(Clone,Debug,Default)]
|
||||
pub struct InputState{
|
||||
mouse:MouseState,
|
||||
next_mouse:MouseState,
|
||||
@ -79,15 +40,10 @@ pub struct InputState{
|
||||
}
|
||||
impl InputState{
|
||||
fn set_next_mouse(&mut self,next_mouse:MouseState){
|
||||
// would this be correct?
|
||||
// if self.next_mouse.time==next_mouse.time{
|
||||
// self.next_mouse=next_mouse;
|
||||
// }else{
|
||||
//I like your functions magic language
|
||||
self.mouse=std::mem::replace(&mut self.next_mouse,next_mouse);
|
||||
//equivalently:
|
||||
//(self.next_mouse,self.mouse)=(next_mouse,self.next_mouse.clone());
|
||||
// }
|
||||
}
|
||||
fn replace_mouse(&mut self,mouse:MouseState,next_mouse:MouseState){
|
||||
(self.next_mouse,self.mouse)=(next_mouse,mouse);
|
||||
@ -109,15 +65,6 @@ impl InputState{
|
||||
((dm*t)/dt).as_ivec2()
|
||||
}
|
||||
}
|
||||
impl Default for InputState{
|
||||
fn default()->Self{
|
||||
Self{
|
||||
mouse:MouseState{pos:Default::default(),time:Time::ZERO-Time::EPSILON*2},
|
||||
next_mouse:MouseState{pos:Default::default(),time:Time::ZERO-Time::EPSILON},
|
||||
controls:Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
enum JumpDirection{
|
||||
Exactly(Planar64Vec3),
|
||||
@ -200,17 +147,17 @@ fn ground_things(walk_settings:&gameplay_style::WalkSettings,contact:&ContactCol
|
||||
let normal=contact_normal(models,hitbox_mesh,contact);
|
||||
let gravity=touching.base_acceleration(models,style,camera,input_state);
|
||||
let control_dir=style.get_y_control_dir(camera,input_state.controls);
|
||||
let target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal);
|
||||
let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity);
|
||||
(gravity,target_velocity_clipped)
|
||||
let mut target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal);
|
||||
touching.constrain_velocity(models,hitbox_mesh,&mut target_velocity);
|
||||
(gravity,target_velocity)
|
||||
}
|
||||
fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&ContactCollision,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState)->(Planar64Vec3,Planar64Vec3){
|
||||
let normal=contact_normal(models,hitbox_mesh,contact);
|
||||
let gravity=touching.base_acceleration(models,style,camera,input_state);
|
||||
let control_dir=style.get_y_control_dir(camera,input_state.controls);
|
||||
let target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal);
|
||||
let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity);
|
||||
(gravity,target_velocity_clipped)
|
||||
let mut target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal);
|
||||
touching.constrain_velocity(models,hitbox_mesh,&mut target_velocity);
|
||||
(gravity,target_velocity)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -807,8 +754,8 @@ impl TouchingState{
|
||||
//TODO: add water
|
||||
a
|
||||
}
|
||||
fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:Planar64Vec3)->Planar64Vec3{
|
||||
let contacts:Vec<_>=self.contacts.iter().map(|contact|{
|
||||
fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:&mut Planar64Vec3){
|
||||
let contacts=self.contacts.iter().map(|contact|{
|
||||
let n=contact_normal(models,hitbox_mesh,contact);
|
||||
crate::push_solve::Contact{
|
||||
position:vec3::ZERO,
|
||||
@ -816,10 +763,10 @@ impl TouchingState{
|
||||
normal:n,
|
||||
}
|
||||
}).collect();
|
||||
crate::push_solve::push_solve(&contacts,velocity)
|
||||
*velocity=crate::push_solve::push_solve(&contacts,*velocity);
|
||||
}
|
||||
fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:Planar64Vec3)->Planar64Vec3{
|
||||
let contacts:Vec<_>=self.contacts.iter().map(|contact|{
|
||||
fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:&mut Planar64Vec3){
|
||||
let contacts=self.contacts.iter().map(|contact|{
|
||||
let n=contact_normal(models,hitbox_mesh,contact);
|
||||
crate::push_solve::Contact{
|
||||
position:vec3::ZERO,
|
||||
@ -827,16 +774,15 @@ impl TouchingState{
|
||||
normal:n,
|
||||
}
|
||||
}).collect();
|
||||
crate::push_solve::push_solve(&contacts,acceleration)
|
||||
*acceleration=crate::push_solve::push_solve(&contacts,*acceleration);
|
||||
}
|
||||
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
|
||||
// let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
|
||||
let relative_body=body;
|
||||
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){
|
||||
let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
|
||||
for contact in &self.contacts{
|
||||
//detect face slide off
|
||||
let model_mesh=models.contact_mesh(contact);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time,collector.time(),contact.face_id).map(|(_face,time)|{
|
||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(_face,time)|{
|
||||
TimedInstruction{
|
||||
time:relative_body.time+time.into(),
|
||||
instruction:InternalInstruction::CollisionEnd(
|
||||
@ -850,7 +796,7 @@ impl TouchingState{
|
||||
//detect model collision in reverse
|
||||
let model_mesh=models.intersect_mesh(intersect);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_out(&relative_body,start_time,collector.time()).map(|(_face,time)|{
|
||||
collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{
|
||||
TimedInstruction{
|
||||
time:relative_body.time+time.into(),
|
||||
instruction:InternalInstruction::CollisionEnd(
|
||||
@ -914,9 +860,6 @@ impl PhysicsState{
|
||||
pub const fn mode(&self)->gameplay_modes::ModeId{
|
||||
self.mode_state.get_mode_id()
|
||||
}
|
||||
pub fn get_finish_time(&self)->Option<run::Time>{
|
||||
self.run.get_finish_time()
|
||||
}
|
||||
pub fn clear(&mut self){
|
||||
self.touching.clear();
|
||||
}
|
||||
@ -1168,7 +1111,7 @@ impl PhysicsData{
|
||||
//this is the one who asks
|
||||
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
|
||||
//JUST POLLING!!! NO MUTATION
|
||||
let mut collector=instruction::InstructionCollector::new(time_limit);
|
||||
let mut collector = instruction::InstructionCollector::new(time_limit);
|
||||
|
||||
collector.collect(state.next_move_instruction());
|
||||
|
||||
@ -1185,7 +1128,7 @@ impl PhysicsData{
|
||||
//no checks are needed because of the time limits.
|
||||
let model_mesh=data.models.mesh(convex_mesh_id);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_in(relative_body,state.time,collector.time())
|
||||
collector.collect(minkowski.predict_collision_in(relative_body,collector.time())
|
||||
//temp (?) code to avoid collision loops
|
||||
.map_or(None,|(face,dt)|{
|
||||
// this must be rounded to avoid the infinite loop when hitting the start zone
|
||||
@ -1202,7 +1145,7 @@ impl PhysicsData{
|
||||
)
|
||||
);
|
||||
});
|
||||
collector.take()
|
||||
collector.instruction()
|
||||
}
|
||||
|
||||
|
||||
@ -1294,17 +1237,19 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
|
||||
let mut culled=false;
|
||||
touching.contacts.retain(|contact|{
|
||||
let n=contact_normal(models,hitbox_mesh,contact);
|
||||
let r=(n.dot(v)>>52).is_positive();
|
||||
let r=n.dot(v).is_positive();
|
||||
if r{
|
||||
culled=true;
|
||||
println!("set_velocity_cull contact={:?}",contact);
|
||||
}
|
||||
!r
|
||||
});
|
||||
set_velocity(body,touching,models,hitbox_mesh,v);
|
||||
culled
|
||||
}
|
||||
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){
|
||||
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);;
|
||||
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,mut v:Planar64Vec3){
|
||||
touching.constrain_velocity(models,hitbox_mesh,&mut v);
|
||||
body.velocity=v;
|
||||
}
|
||||
fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{
|
||||
//This is not correct but is better than what I have
|
||||
@ -1314,14 +1259,16 @@ fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&Phys
|
||||
let r=n.dot(a).is_positive();
|
||||
if r{
|
||||
culled=true;
|
||||
println!("set_acceleration_cull contact={:?}",contact);
|
||||
}
|
||||
!r
|
||||
});
|
||||
set_acceleration(body,touching,models,hitbox_mesh,a);
|
||||
culled
|
||||
}
|
||||
fn set_acceleration(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3){
|
||||
body.acceleration=touching.constrain_acceleration(models,hitbox_mesh,a);
|
||||
fn set_acceleration(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,mut a:Planar64Vec3){
|
||||
touching.constrain_acceleration(models,hitbox_mesh,&mut a);
|
||||
body.acceleration=a;
|
||||
}
|
||||
|
||||
fn teleport(
|
||||
@ -1530,7 +1477,7 @@ fn collision_start_contact(
|
||||
let model_id=contact.model_id.into();
|
||||
let mut allow_run_teleport_behaviour=not_spawn_at(mode,model_id);
|
||||
match &attr.contacting.contact_behaviour{
|
||||
Some(gameplay_attributes::ContactingBehaviour::Surf)=>(),
|
||||
Some(gameplay_attributes::ContactingBehaviour::Surf)=>println!("I'm surfing!"),
|
||||
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
|
||||
&Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{
|
||||
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1();
|
||||
@ -1560,21 +1507,6 @@ fn collision_start_contact(
|
||||
}
|
||||
},
|
||||
}
|
||||
match &attr.general.trajectory{
|
||||
Some(trajectory)=>{
|
||||
match trajectory{
|
||||
gameplay_attributes::SetTrajectory::AirTime(_)=>todo!(),
|
||||
gameplay_attributes::SetTrajectory::Height(_)=>todo!(),
|
||||
gameplay_attributes::SetTrajectory::TargetPointTime{..}=>todo!(),
|
||||
gameplay_attributes::SetTrajectory::TargetPointSpeed{..}=>todo!(),
|
||||
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>{
|
||||
move_state.cull_velocity(velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
},
|
||||
gameplay_attributes::SetTrajectory::DotVelocity{..}=>todo!(),
|
||||
}
|
||||
},
|
||||
None=>(),
|
||||
}
|
||||
//I love making functions with 10 arguments to dodge the borrow checker
|
||||
if allow_run_teleport_behaviour{
|
||||
run_teleport_behaviour(model_id,attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time);
|
||||
@ -1602,6 +1534,21 @@ fn collision_start_contact(
|
||||
}
|
||||
}
|
||||
}
|
||||
match &attr.general.trajectory{
|
||||
Some(trajectory)=>{
|
||||
match trajectory{
|
||||
gameplay_attributes::SetTrajectory::AirTime(_)=>todo!(),
|
||||
gameplay_attributes::SetTrajectory::Height(_)=>todo!(),
|
||||
gameplay_attributes::SetTrajectory::TargetPointTime { target_point: _, time: _ }=>todo!(),
|
||||
gameplay_attributes::SetTrajectory::TargetPointSpeed { target_point: _, speed: _, trajectory_choice: _ }=>todo!(),
|
||||
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>{
|
||||
move_state.cull_velocity(velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
},
|
||||
gameplay_attributes::SetTrajectory::DotVelocity { direction: _, dot: _ }=>todo!(),
|
||||
}
|
||||
},
|
||||
None=>(),
|
||||
}
|
||||
//doing enum to set the acceleration when surfing
|
||||
//doing input_and_body to refresh the walk state if you hit a wall while accelerating
|
||||
move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
|
||||
@ -1772,19 +1719,19 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
|
||||
|MoveState::Fly
|
||||
=>println!("ReachWalkTargetVelocity fired for non-walking MoveState"),
|
||||
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>{
|
||||
//velocity is already handled by advance_time
|
||||
//we know that the acceleration is precisely zero because the walk target is known to be reachable
|
||||
//which means that gravity can be fully cancelled
|
||||
//ignore moving platforms for now
|
||||
let target=core::mem::replace(&mut walk_state.target,TransientAcceleration::Reached);
|
||||
set_acceleration(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
|
||||
// check what the target was to see if it was invalid
|
||||
match target{
|
||||
match &walk_state.target{
|
||||
//you are not supposed to reach a walk target which is already reached!
|
||||
TransientAcceleration::Reached=>println!("Invalid walk target: Reached"),
|
||||
TransientAcceleration::Reachable{..}=>(),
|
||||
TransientAcceleration::Reached=>unreachable!(),
|
||||
TransientAcceleration::Reachable{acceleration:_,time:_}=>{
|
||||
//velocity is already handled by advance_time
|
||||
//we know that the acceleration is precisely zero because the walk target is known to be reachable
|
||||
//which means that gravity can be fully cancelled
|
||||
//ignore moving platforms for now
|
||||
set_acceleration(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
|
||||
walk_state.target=TransientAcceleration::Reached;
|
||||
},
|
||||
//you are not supposed to reach an unreachable walk target!
|
||||
TransientAcceleration::Unreachable{..}=>println!("Invalid walk target: Unreachable"),
|
||||
TransientAcceleration::Unreachable{acceleration:_}=>unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1843,7 +1790,7 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
let jump_dir=walk_state.jump_direction.direction(&data.models,&data.hitbox_mesh,&walk_state.contact);
|
||||
let booster_option=data.models.contact_attr(walk_state.contact.model_id).general.booster.as_ref();
|
||||
let jumped_velocity=jump_settings.jumped_velocity(&state.style,jump_dir,state.body.velocity,booster_option);
|
||||
state.cull_velocity(data,jumped_velocity);
|
||||
state.cull_velocity(&data,jumped_velocity);
|
||||
}
|
||||
}
|
||||
b_refresh_walk_target=false;
|
||||
@ -1912,9 +1859,8 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::file;
|
||||
use crate::body::VirtualBody;
|
||||
use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
|
||||
use crate::body::VirtualBody;
|
||||
use super::*;
|
||||
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
let h0=HitboxMesh::new(PhysicsMesh::unit_cube(),integer::Planar64Affine3::new(mat3::from_diagonal(int3(5,1,5)>>1),vec3::ZERO));
|
||||
@ -1922,7 +1868,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,Time::from_secs(10));
|
||||
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>){
|
||||
@ -1940,7 +1886,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,Time::from_secs(10));
|
||||
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>){
|
||||
@ -2127,202 +2073,4 @@ mod test{
|
||||
Time::ZERO
|
||||
),None);
|
||||
}
|
||||
#[test]
|
||||
fn run_replay(){
|
||||
println!("loading map file..");
|
||||
let map=file::load("../tools/bhop_maps/5692113331.snfm");
|
||||
println!("loading bot file..");
|
||||
let bot=file::load("../tools/replays/534s+997497968ns.snfb");
|
||||
if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
println!("simulating...");
|
||||
let mut physics=PhysicsState::default();
|
||||
for ins in bot.instructions{
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
|
||||
}
|
||||
match physics.get_finish_time(){
|
||||
Some(time)=>println!("finish time:{}",time),
|
||||
None=>println!("simulation did not end in finished state"),
|
||||
}
|
||||
}else{
|
||||
panic!("missing files");
|
||||
}
|
||||
}
|
||||
enum DeterminismResult{
|
||||
Deterministic,
|
||||
NonDeterministic,
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum ReplayError{
|
||||
Load(file::LoadError),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
impl From<file::LoadError> for ReplayError{
|
||||
fn from(value:file::LoadError)->Self{
|
||||
Self::Load(value)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for ReplayError{
|
||||
fn from(value:std::io::Error)->Self{
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
|
||||
// create default physics state
|
||||
let mut physics_deterministic=PhysicsState::default();
|
||||
// create a second physics state
|
||||
let mut physics_filtered=PhysicsState::default();
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
println!("simulating...");
|
||||
|
||||
let mut non_idle_count=0;
|
||||
|
||||
for (i,ins) in bot.instructions.into_iter().enumerate(){
|
||||
let state_deterministic=physics_deterministic.clone();
|
||||
let state_filtered=physics_filtered.clone();
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
|
||||
match ins{
|
||||
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
|
||||
other=>{
|
||||
non_idle_count+=1;
|
||||
// run
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
|
||||
// check if position matches
|
||||
let b0=physics_deterministic.camera_body();
|
||||
let b1=physics_filtered.camera_body();
|
||||
if b0.position!=b1.position{
|
||||
println!("desync at instruction #{}",i);
|
||||
println!("non idle instructions completed={non_idle_count}");
|
||||
println!("instruction #{i}={:?}",other);
|
||||
println!("deterministic state0:\n{state_deterministic:?}");
|
||||
println!("filtered state0:\n{state_filtered:?}");
|
||||
println!("deterministic state1:\n{:?}",physics_deterministic);
|
||||
println!("filtered state1:\n{:?}",physics_filtered);
|
||||
return DeterminismResult::NonDeterministic;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
match physics_deterministic.get_finish_time(){
|
||||
Some(time)=>println!("[with idle] finish time:{}",time),
|
||||
None=>println!("[with idle] simulation did not end in finished state"),
|
||||
}
|
||||
match physics_filtered.get_finish_time(){
|
||||
Some(time)=>println!("[filtered] finish time:{}",time),
|
||||
None=>println!("[filtered] simulation did not end in finished state"),
|
||||
}
|
||||
DeterminismResult::Deterministic
|
||||
}
|
||||
type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>;
|
||||
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
|
||||
s.spawn(move ||{
|
||||
let result=match file::load(file_path.as_path()){
|
||||
Ok(file::LoadFormat::Bot(bot))=>{
|
||||
println!("Running {:?}",file_path.file_stem());
|
||||
Ok(Some(segment_determinism(bot,physics_data)))
|
||||
},
|
||||
Ok(_)=>{
|
||||
println!("Provided bot file is not a bot file!");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e)=>{
|
||||
println!("Load error");
|
||||
Err(e)
|
||||
},
|
||||
};
|
||||
// send when thread is complete
|
||||
send.send(result).unwrap();
|
||||
});
|
||||
}
|
||||
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
|
||||
Ok(dir_entry.file_type()?.is_file().then_some(
|
||||
dir_entry.path()
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_determinism()->Result<(),ReplayError>{
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
println!("loading map file..");
|
||||
let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{
|
||||
panic!("Provided map file is not a map file!");
|
||||
};
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let (send,recv)=std::sync::mpsc::channel();
|
||||
|
||||
let mut read_dir=std::fs::read_dir("../tools/replays")?;
|
||||
|
||||
// promise that &physics_data will outlive the spawned threads
|
||||
let thread_results=std::thread::scope(|s|{
|
||||
let mut thread_results=Vec::new();
|
||||
|
||||
// spawn threads
|
||||
println!("spawning up to {thread_limit} threads...");
|
||||
let mut active_thread_count=0;
|
||||
while active_thread_count<thread_limit{
|
||||
if let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
active_thread_count+=1;
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// spawn another thread every time a message is received from the channel
|
||||
println!("riding parallelism wave...");
|
||||
while let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
// wait for a thread to complete
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for remaining threads to complete
|
||||
println!("waiting for all threads to complete...");
|
||||
for _ in 0..active_thread_count{
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
}
|
||||
|
||||
println!("done.");
|
||||
Ok::<_,ReplayError>(thread_results)
|
||||
})?;
|
||||
|
||||
// tally results
|
||||
#[derive(Default)]
|
||||
struct Totals{
|
||||
deterministic:u32,
|
||||
nondeterministic:u32,
|
||||
invalid:u32,
|
||||
error:u32,
|
||||
}
|
||||
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
|
||||
match result{
|
||||
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
|
||||
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
|
||||
Ok(None)=>totals.invalid+=1,
|
||||
Err(_)=>totals.error+=1,
|
||||
}
|
||||
totals
|
||||
});
|
||||
|
||||
println!("deterministic={deterministic}");
|
||||
println!("nondeterministic={nondeterministic}");
|
||||
println!("invalid={invalid}");
|
||||
println!("error={error}");
|
||||
|
||||
assert!(nondeterministic==0);
|
||||
assert!(invalid==0);
|
||||
assert!(error==0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +289,7 @@ fn get_best_push_ray_and_conts<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
|
||||
fn get_first_touch<'a>(contacts:&'a Vec<Contact>,ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
|
||||
contacts.iter()
|
||||
.filter(|&contact|
|
||||
!conts.iter().any(|&c|std::ptr::eq(c,contact))
|
||||
@ -299,7 +299,7 @@ fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ra
|
||||
.min_by_key(|&(t,_)|t)
|
||||
}
|
||||
|
||||
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
|
||||
pub fn push_solve(contacts:&Vec<Contact>,point:Planar64Vec3)->Planar64Vec3{
|
||||
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
|
||||
loop{
|
||||
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){
|
||||
|
@ -90,7 +90,7 @@ pub struct Recording{
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
}
|
||||
impl Recording{
|
||||
pub fn new(
|
||||
fn new(
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
)->Self{
|
||||
Self{instructions}
|
||||
@ -292,16 +292,11 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
Instruction::Control(SessionControlInstruction::SaveReplay)=>{
|
||||
// Bind: N
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
let file=std::fs::File::create(format!("{}.snfb",ins.time)).unwrap();
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
let file_name=format!("replays/{}.snfb",ins.time);
|
||||
std::thread::spawn(move ||{
|
||||
std::fs::create_dir_all("replays").unwrap();
|
||||
let file=std::fs::File::create(file_name).unwrap();
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),crate::physics::VERSION.get(),replay.recording.instructions).unwrap();
|
||||
println!("Finished writing bot file!");
|
||||
});
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),replay.recording.instructions).unwrap();
|
||||
},
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
|
@ -1,7 +1,7 @@
|
||||
use strafesnet_common::instruction::TimedInstruction;
|
||||
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
|
||||
use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
|
||||
use crate::file::LoadFormat;
|
||||
use crate::file::Format2;
|
||||
use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
|
||||
use crate::session::{SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
|
||||
|
||||
@ -30,8 +30,8 @@ impl WindowContext<'_>{
|
||||
match event{
|
||||
winit::event::WindowEvent::DroppedFile(path)=>{
|
||||
match crate::file::load(path.as_path()){
|
||||
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(),
|
||||
Ok(Format2::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
|
||||
Ok(Format2::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}).unwrap(),
|
||||
Err(e)=>println!("Failed to load file: {e}"),
|
||||
}
|
||||
},
|
||||
|
@ -1 +1 @@
|
||||
mangohud ../target/release/strafe-client bhop_maps/5692113331.snfm "$@"
|
||||
mangohud ../target/release/strafe-client bhop_maps/5692113331.snfm
|
||||
|
@ -1 +1 @@
|
||||
mangohud ../target/release/strafe-client bhop_maps/5692124338.snfm "$@"
|
||||
mangohud ../target/release/strafe-client bhop_maps/5692124338.snfm
|
||||
|
@ -1 +0,0 @@
|
||||
/run/media/quat/Files/Documents/map-files/verify-scripts/replays
|
@ -1 +1 @@
|
||||
mangohud ../target/release/strafe-client "$@"
|
||||
mangohud ../target/release/strafe-client "$1"
|
||||
|
@ -1 +1 @@
|
||||
mangohud ../target/release/strafe-client bhop_maps/5692152916.snfm "$@"
|
||||
mangohud ../target/release/strafe-client bhop_maps/5692152916.snfm
|
||||
|
@ -1 +1 @@
|
||||
mangohud ../target/release/strafe-client surf_maps/5692145408.snfm "$@"
|
||||
mangohud ../target/release/strafe-client surf_maps/5692145408.snfm
|
||||
|
Reference in New Issue
Block a user