23 Commits

Author SHA1 Message Date
019c7b3887 wip 2024-08-19 12:40:32 -07:00
bc9599eef0 remove unnecessary work from bvh algorithm 2024-08-18 15:41:42 -07:00
88b87b1859 v0.4.0 typed attribute structs 2024-08-09 14:23:59 -07:00
aa79d9a54e update deps 2024-08-09 14:23:09 -07:00
d95549070b gameplay_attributes: split out collision variants into structs 2024-08-09 13:49:42 -07:00
af1f1aad14 bvh: don't require T to implement Default 2024-08-09 13:48:20 -07:00
8012a459bf v0.3.0 rework jumping and boosters 2024-08-07 18:13:24 -07:00
22216846e3 refactor JumpCalculation 2024-08-07 18:10:10 -07:00
eebde9b54a this should all be pub 2024-08-07 15:11:41 -07:00
7d1058164b rename JumpImpulse variant 2024-08-07 14:40:59 -07:00
415be69ba8 zeroes: use arrayvec for our stack allocated variable length array with a fixed capacity 2024-08-07 12:46:43 -07:00
62e9397c98 zeroes: branch instead of heap allocation 2024-08-07 11:26:25 -07:00
9563290454 v0.2.3 physics instructions 2024-08-06 10:49:43 -07:00
a2d9e97467 add reset instruction + physics::Instruction is Clone 2024-08-05 17:26:15 -07:00
62a6af9261 pull instruction types out of physics 2024-08-02 13:45:52 -07:00
a819f93fa9 const opportunity 2024-08-02 13:45:10 -07:00
0375844f4f v0.2.2 timer tweaks 2024-08-02 10:40:07 -07:00
0dd41ae2f1 more functions 2024-08-02 10:35:29 -07:00
fed810334b simpler 2024-08-01 21:55:53 -07:00
0ff4de3d38 meant to make this public 2024-08-01 09:42:08 -07:00
0c0365746e v0.2.1 timers + runs 2024-08-01 09:32:40 -07:00
0c4a34c9cb runs 2024-08-01 09:31:26 -07:00
1318fd2856 timers 2024-08-01 09:30:35 -07:00
12 changed files with 756 additions and 216 deletions

21
Cargo.lock generated

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "bitflags"
version = "2.6.0"
@ -27,26 +33,27 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.78"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strafesnet_common"
version = "0.2.0"
version = "0.4.0"
dependencies = [
"arrayvec",
"bitflags",
"glam",
"id",
@ -54,9 +61,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.52"
version = "2.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
dependencies = [
"proc-macro2",
"quote",

@ -1,6 +1,6 @@
[package]
name = "strafesnet_common"
version = "0.2.0"
version = "0.4.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/common"
license = "MIT OR Apache-2.0"
@ -10,6 +10,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
arrayvec = "0.7.4"
bitflags = "2.6.0"
glam = "0.28.0"
id = { version = "0.1.0", registry = "strafesnet" }

@ -1,14 +1,10 @@
use crate::aabb::Aabb;
//da algaritum
//lista boxens
//sort by {minx,maxx,miny,maxy,minz,maxz} (6 lists)
//find the sets that minimizes the sum of surface areas
//splitting is done when the minimum split sum of surface areas is larger than the node's own surface area
//start with bisection into octrees because a bad bvh is still 1000x better than no bvh
//sort the centerpoints on each axis (3 lists)
//bv is put into octant based on whether it is upper or lower in each list
//bvh 2
//sort {x,y,z}_{min,max} (6 sets)
//octree partitioning
//for each axis, minimize the sum of the lower and upper partition world width
//effectively, find the biggest gap
pub enum RecursiveContent<R,T>{
Branch(Vec<R>),
@ -19,11 +15,18 @@ impl<R,T> Default for RecursiveContent<R,T>{
Self::Branch(Vec::new())
}
}
#[derive(Default)]
pub struct BvhNode<T>{
content:RecursiveContent<BvhNode<T>,T>,
aabb:Aabb,
}
impl<T> Default for BvhNode<T>{
fn default()->Self{
Self{
content:Default::default(),
aabb:Aabb::default(),
}
}
}
pub struct BvhWeightNode<W,T>{
content:RecursiveContent<BvhWeightNode<W,T>,T>,
weight:W,
@ -114,44 +117,64 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
aabb,
}
}else{
let mut octant=std::collections::HashMap::with_capacity(n);//this ids which octant the boxen is put in
let mut sort_x=Vec::with_capacity(n);
let mut sort_y=Vec::with_capacity(n);
let mut sort_z=Vec::with_capacity(n);
for (i,(_,aabb)) in boxen.iter().enumerate(){
let center=aabb.center();
octant.insert(i,0);
sort_x.push((i,center.x()));
sort_y.push((i,center.y()));
sort_z.push((i,center.z()));
}
sort_x.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_y.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_z.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
let mut sort_x_min=boxen.iter()
.enumerate()
.map(|(i,(_,aabb))|(i,aabb))
.collect::<Vec<(usize,&Aabb)>>();
let mut sort_x_max=sort_x_min.clone();
let mut sort_y_min=sort_x_min.clone();
let mut sort_y_max=sort_x_min.clone();
let mut sort_z_min=sort_x_min.clone();
let mut sort_z_max=sort_x_min.clone();
//sort by x_min, but if x_min is equal, then sort by x_max reverse instead
sort_x_min.sort_by(|&(_,aabb0),&(_,aabb1)|
aabb0.min().x().cmp(&aabb1.min().x())
.then(aabb0.max().x().cmp(&aabb1.max().x()).reverse())
);
sort_x_max.sort_by(|&(_,aabb0),&(_,aabb1)|
aabb0.max().x().cmp(&aabb1.max().x())
.then(aabb0.min().x().cmp(&aabb1.min().x()).reverse())
);
sort_y_min.sort_by(|&(_,aabb0),&(_,aabb1)|
aabb0.min().y().cmp(&aabb1.min().y())
.then(aabb0.max().y().cmp(&aabb1.max().y()).reverse())
);
sort_y_max.sort_by(|&(_,aabb0),&(_,aabb1)|
aabb0.max().y().cmp(&aabb1.max().y())
.then(aabb0.min().y().cmp(&aabb1.min().y()).reverse())
);
sort_z_min.sort_by(|&(_,aabb0),&(_,aabb1)|
aabb0.min().z().cmp(&aabb1.min().z())
.then(aabb0.max().z().cmp(&aabb1.max().z()).reverse())
);
sort_z_max.sort_by(|&(_,aabb0),&(_,aabb1)|
aabb0.max().z().cmp(&aabb1.max().z())
.then(aabb0.min().z().cmp(&aabb1.min().z()).reverse())
);
let h=n/2;
let median_x=sort_x[h].1;
let median_y=sort_y[h].1;
let median_z=sort_z[h].1;
for (i,c) in sort_x{
if median_x<c{
octant.insert(i,octant[&i]+1<<0);
}
//partition point gives the first index for which the predicate evaluates to false
let first_index_gt_median_x=sort_x.partition_point(|tup|!(median_x<tup.1));
let first_index_gt_median_y=sort_y.partition_point(|tup|!(median_y<tup.1));
let first_index_gt_median_z=sort_z.partition_point(|tup|!(median_z<tup.1));
//this ids which octant the boxen is put in
let mut octant=vec![0;n];
for &(i,_) in &sort_x[first_index_gt_median_x..]{
octant[i]+=1<<0;
}
for (i,c) in sort_y{
if median_y<c{
octant.insert(i,octant[&i]+1<<1);
}
for &(i,_) in &sort_y[first_index_gt_median_y..]{
octant[i]+=1<<1;
}
for (i,c) in sort_z{
if median_z<c{
octant.insert(i,octant[&i]+1<<2);
}
for &(i,_) in &sort_z[first_index_gt_median_z..]{
octant[i]+=1<<2;
}
//generate lists for unique octant values
let mut list_list=Vec::with_capacity(8);
let mut octant_list=Vec::with_capacity(8);
for (i,(data,aabb)) in boxen.into_iter().enumerate(){
let octant_id=octant[&i];
let octant_id=octant[i];
let list_id=if let Some(list_id)=octant_list.iter().position(|&id|id==octant_id){
list_id
}else{

@ -31,6 +31,22 @@ pub enum Booster{
//Affine(crate::integer::Planar64Affine3),//capable of SetVelocity,DotVelocity,normal booster,bouncy part,redirect velocity, and much more
Velocity(Planar64Vec3),//straight up boost velocity adds to your current velocity
Energy{direction:Planar64Vec3,energy:Planar64},//increase energy in direction
AirTime(Time),//increase airtime, invariant across mass and gravity changes
Height(Planar64),//increase height, invariant across mass and gravity changes
}
impl Booster{
pub fn boost(&self,velocity:Planar64Vec3)->Planar64Vec3{
match self{
&Booster::Velocity(boost_velocity)=>velocity+boost_velocity,
&Booster::Energy{direction,energy}=>{
let d=direction.dot(velocity);
//TODO: think about negative
velocity+direction.with_length((d*d+energy).sqrt()-d)
},
Booster::AirTime(_)=>todo!(),
Booster::Height(_)=>todo!(),
}
}
}
#[derive(Clone,Hash,Eq,PartialEq)]
pub enum TrajectoryChoice{
@ -134,23 +150,24 @@ impl IntersectingAttributes{
}
#[derive(Clone,Copy,id::Id,Hash,Eq,PartialEq)]
pub struct CollisionAttributesId(u32);
#[derive(Clone,Default,Hash,Eq,PartialEq)]
pub struct ContactAttributes{
pub contacting:ContactingAttributes,
pub general:GeneralAttributes,
}
#[derive(Clone,Default,Hash,Eq,PartialEq)]
pub struct IntersectAttributes{
pub intersecting:IntersectingAttributes,
pub general:GeneralAttributes,
}
#[derive(Clone,Hash,Eq,PartialEq)]
pub enum CollisionAttributes{
Decoration,//visual only
Contact{//track whether you are contacting the object
contacting:ContactingAttributes,
general:GeneralAttributes,
},
Intersect{//track whether you are intersecting the object
intersecting:IntersectingAttributes,
general:GeneralAttributes,
},
Contact(ContactAttributes),//track whether you are contacting the object
Intersect(IntersectAttributes),//track whether you are intersecting the object
}
impl CollisionAttributes{
pub fn contact_default()->Self{
Self::Contact{
contacting:ContactingAttributes::default(),
general:GeneralAttributes::default()
}
Self::Contact(ContactAttributes::default())
}
}

@ -41,33 +41,135 @@ impl std::default::Default for StyleModifiers{
#[derive(Clone,Debug)]
pub enum JumpCalculation{
Capped,//roblox
Energy,//new
Linear,//source
Max,//Roblox: jumped_speed=max(velocity.boost(),velocity.jump())
BoostThenJump,//jumped_speed=velocity.boost().jump()
JumpThenBoost,//jumped_speed=velocity.jump().boost()
}
#[derive(Clone,Debug)]
pub enum JumpImpulse{
FromTime(Time),//jump time is invariant across mass and gravity changes
FromHeight(Planar64),//jump height is invariant across mass and gravity changes
FromDeltaV(Planar64),//jump velocity is invariant across mass and gravity changes
FromEnergy(Planar64),// :)
Time(Time),//jump time is invariant across mass and gravity changes
Height(Planar64),//jump height is invariant across mass and gravity changes
Linear(Planar64),//jump velocity is invariant across mass and gravity changes
Energy(Planar64),// :)
}
//Jumping acts on dot(walks_state.normal,body.velocity)
//Capped means it increases the dot to the cap
//Energy means it adds energy
//Linear means it linearly adds on
impl JumpImpulse{
//fn get_jump_time(&self)->Planar64
//fn get_jump_height(&self)->Planar64
//fn get_jump_energy(&self)->Planar64
pub fn jump(
&self,
velocity:Planar64Vec3,
jump_dir:Planar64Vec3,
gravity:&Planar64Vec3,
mass:Planar64,
)->Planar64Vec3{
match self{
&JumpImpulse::Time(time)=>velocity-*gravity*time,
&JumpImpulse::Height(height)=>{
//height==-v.y*v.y/(2*g.y);
//use energy to determine max height
let g=gravity.length();
let v_g=gravity.dot(velocity)/g;
//do it backwards
velocity-gravity.with_length((v_g*v_g+height*g*2).sqrt()+v_g)
},
&JumpImpulse::Linear(jump_speed)=>velocity+jump_dir.with_length(jump_speed),
&JumpImpulse::Energy(energy)=>{
//calculate energy
let e=gravity.dot(velocity);
//add
//you get the idea
todo!()
},
}
}
//TODO: remove this and implement JumpCalculation properly
pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
match self{
&JumpImpulse::FromTime(time)=>gravity.length()*(time/2),
&JumpImpulse::FromHeight(height)=>(gravity.length()*height*2).sqrt(),
&JumpImpulse::FromDeltaV(deltav)=>deltav,
&JumpImpulse::FromEnergy(energy)=>(energy*2/mass).sqrt(),
&JumpImpulse::Time(time)=>gravity.length()*(time/2),
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt(),
&JumpImpulse::Linear(deltav)=>deltav,
&JumpImpulse::Energy(energy)=>(energy*2/mass).sqrt(),
}
}
}
#[derive(Clone,Debug)]
pub struct JumpSettings{
//information used to calculate jump power
pub impulse:JumpImpulse,
//information used to calculate jump behaviour
pub calculation:JumpCalculation,
//limit the minimum jump power when combined with downwards momentum
//This is true in both roblox and source
pub limit_minimum:bool,
}
impl JumpSettings{
pub fn jumped_velocity(
&self,
style:&StyleModifiers,
jump_dir:Planar64Vec3,
rel_velocity:Planar64Vec3,
booster:Option<&crate::gameplay_attributes::Booster>,
)->Planar64Vec3{
let jump_speed=self.impulse.get_jump_deltav(&style.gravity,style.mass);
match (self.limit_minimum,&self.calculation){
(true,JumpCalculation::Max)=>{
//the roblox calculation
let boost_vel=match booster{
Some(booster)=>booster.boost(rel_velocity),
None=>rel_velocity,
};
let j=boost_vel.dot(jump_dir);
if j<jump_speed{
//weak booster: just do a regular jump
boost_vel+jump_dir.with_length(jump_speed-j)
}else{
//activate booster normally, jump does nothing
boost_vel
}
},
(true,_)=>{
//the source calculation (?)
let boost_vel=match booster{
Some(booster)=>booster.boost(rel_velocity),
None=>rel_velocity,
};
let j=boost_vel.dot(jump_dir);
if j<jump_speed{
//speed in direction of jump cannot be lower than amount
boost_vel+jump_dir.with_length(jump_speed-j)
}else{
//boost and jump add together
boost_vel+jump_dir.with_length(jump_speed)
}
}
(false,JumpCalculation::Max)=>{
//??? calculation
//max(boost_vel,jump_vel)
let boost_vel=match booster{
Some(booster)=>booster.boost(rel_velocity),
None=>rel_velocity,
};
let boost_dot=boost_vel.dot(jump_dir);
if boost_dot<jump_speed{
//weak boost is extended to jump speed
boost_vel+jump_dir.with_length(jump_speed-boost_dot)
}else{
//activate booster normally, jump does nothing
boost_vel
}
},
//the strafe client calculation
(false,_)=>{
let boost_vel=match booster{
Some(booster)=>booster.boost(rel_velocity),
None=>rel_velocity,
};
boost_vel+jump_dir.with_length(jump_speed)
},
}
}
}
@ -75,34 +177,14 @@ impl JumpImpulse{
#[derive(Clone,Debug)]
pub struct ControlsActivation{
//allowed keys
controls_mask:Controls,
pub controls_mask:Controls,
//allow strafing only if any of the masked controls are held, eg W|S for shsw
controls_intersects:Controls,
pub controls_intersects:Controls,
//allow strafing only if all of the masked controls are held, eg W for hsw, w-only
controls_contains:Controls,
pub controls_contains:Controls,
//Function(Box<dyn Fn(u32)->bool>),
}
impl ControlsActivation{
pub const fn new(
controls_mask:Controls,
controls_intersects:Controls,
controls_contains:Controls,
)->Self{
Self{
controls_mask,
controls_intersects,
controls_contains,
}
}
pub const fn controls_mask(&self)->Controls{
self.controls_mask
}
pub const fn controls_intersects(&self)->Controls{
self.controls_intersects
}
pub const fn controls_contains(&self)->Controls{
self.controls_contains
}
pub const fn mask(&self,controls:Controls)->Controls{
controls.intersection(self.controls_mask)
}
@ -171,23 +253,12 @@ impl ControlsActivation{
#[derive(Clone,Debug)]
pub struct StrafeSettings{
enable:ControlsActivation,
mv:Planar64,
air_accel_limit:Option<Planar64>,
tick_rate:Ratio64,
pub enable:ControlsActivation,
pub mv:Planar64,
pub air_accel_limit:Option<Planar64>,
pub tick_rate:Ratio64,
}
impl StrafeSettings{
pub const fn new(
enable:ControlsActivation,
mv:Planar64,
air_accel_limit:Option<Planar64>,
tick_rate:Ratio64,
)->Self{
Self{enable,mv,air_accel_limit,tick_rate}
}
pub fn into_inner(self)->(ControlsActivation,Planar64,Option<Planar64>,Ratio64){
(self.enable,self.mv,self.air_accel_limit,self.tick_rate)
}
pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
let d=velocity.dot(control_dir);
match d<self.mv{
@ -208,88 +279,28 @@ impl StrafeSettings{
#[derive(Clone,Debug)]
pub struct PropulsionSettings{
magnitude:Planar64,
pub magnitude:Planar64,
}
impl PropulsionSettings{
pub const fn new(magnitude:Planar64)->Self{
Self{magnitude}
}
pub fn magnitude(&self)->Planar64{
self.magnitude
}
pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
control_dir*self.magnitude
}
}
#[derive(Clone,Debug)]
pub struct JumpSettings{
//information used to calculate jump power
impulse:JumpImpulse,
//information used to calculate jump behaviour
calculation:JumpCalculation,
}
impl JumpSettings{
pub const fn new(
impulse:JumpImpulse,
calculation:JumpCalculation,
)->Self{
Self{impulse,calculation}
}
pub fn into_inner(self)->(JumpImpulse,JumpCalculation){
(self.impulse,self.calculation)
}
pub fn jumped_velocity(&self,style:&StyleModifiers,jump_dir:Planar64Vec3,velocity:Planar64Vec3)->Planar64Vec3{
match self.calculation{
//roblox style
JumpCalculation::Capped=>todo!(),
//something different
JumpCalculation::Energy=>todo!(),
//source style
JumpCalculation::Linear=>velocity+jump_dir*(self.impulse.get_jump_deltav(&style.gravity,style.mass)/jump_dir.length()),
}
}
}
#[derive(Clone,Debug)]
pub struct AccelerateSettings{
accel:Planar64,
topspeed:Planar64,
}
impl AccelerateSettings{
pub const fn new(
accel:Planar64,
topspeed:Planar64,
)->Self{
Self{accel,topspeed}
}
pub const fn accel(&self)->Planar64{
self.accel
}
pub const fn topspeed(&self)->Planar64{
self.topspeed
}
pub accel:Planar64,
pub topspeed:Planar64,
}
#[derive(Clone,Debug)]
pub struct WalkSettings{
accelerate:AccelerateSettings,
static_friction:Planar64,
kinetic_friction:Planar64,
pub accelerate:AccelerateSettings,
pub static_friction:Planar64,
pub kinetic_friction:Planar64,
//if a surf slope angle does not exist, then everything is slippery and walking is impossible
surf_dot:Planar64,//surf_dot<n.dot(up)/n.length()
pub surf_dot:Planar64,//surf_dot<n.dot(up)/n.length()
}
impl WalkSettings{
pub const fn new(
accelerate:AccelerateSettings,
static_friction:Planar64,
kinetic_friction:Planar64,
surf_dot:Planar64,
)->Self{
Self{accelerate,static_friction,kinetic_friction,surf_dot}
}
pub fn into_inner(self)->(AccelerateSettings,Planar64,Planar64,Planar64){
(self.accelerate,self.static_friction,self.kinetic_friction,self.surf_dot)
}
pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
//TODO: fallible walk accel
let diff_len=target_diff.length();
@ -329,21 +340,12 @@ impl WalkSettings{
#[derive(Clone,Debug)]
pub struct LadderSettings{
accelerate:AccelerateSettings,
pub accelerate:AccelerateSettings,
//how close to pushing directly into/out of the ladder normal
//does your input need to be to redirect straight up/down the ladder
dot:Planar64,
pub dot:Planar64,
}
impl LadderSettings{
pub const fn new(
accelerate:AccelerateSettings,
dot:Planar64,
)->Self{
Self{accelerate,dot}
}
pub fn into_inner(self)->(AccelerateSettings,Planar64){
(self.accelerate,self.dot)
}
pub const fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
//TODO: fallible ladder accel
self.accelerate.accel
@ -425,8 +427,9 @@ impl StyleModifiers{
tick_rate:Ratio64::new(64,Time::ONE_SECOND.nanos() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::FromEnergy(Planar64::int(512)),
calculation:JumpCalculation::Energy,
impulse:JumpImpulse::Energy(Planar64::int(512)),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:false,
}),
gravity:Planar64Vec3::int(0,-80,0),
mass:Planar64::int(1),
@ -466,8 +469,9 @@ impl StyleModifiers{
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::FromTime(Time::from_micros(715_588)),
calculation:JumpCalculation::Linear,//Should be capped
impulse:JumpImpulse::Time(Time::from_micros(715_588)),
calculation:JumpCalculation::Max,
limit_minimum:true,
}),
gravity:Planar64Vec3::int(0,-100,0),
mass:Planar64::int(1),
@ -522,8 +526,9 @@ impl StyleModifiers{
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::FromHeight(Planar64::int(52)*VALVE_SCALE),
calculation:JumpCalculation::Linear,
impulse:JumpImpulse::Height(Planar64::int(52)*VALVE_SCALE),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:true,
}),
gravity:Planar64Vec3::int(0,-800,0)*VALVE_SCALE,
mass:Planar64::int(1),
@ -562,8 +567,9 @@ impl StyleModifiers{
tick_rate:Ratio64::new(66,Time::ONE_SECOND.nanos() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::FromHeight(Planar64::int(52)*VALVE_SCALE),
calculation:JumpCalculation::Linear,
impulse:JumpImpulse::Height(Planar64::int(52)*VALVE_SCALE),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:true,
}),
gravity:Planar64Vec3::int(0,-800,0)*VALVE_SCALE,
mass:Planar64::int(1),

@ -19,14 +19,14 @@ pub struct InstructionCollector<I>{
instruction:Option<I>,
}
impl<I> InstructionCollector<I>{
pub fn new(time:Time)->Self{
pub const fn new(time:Time)->Self{
Self{
time,
instruction:None
}
}
#[inline]
pub fn time(&self)->Time{
pub const fn time(&self)->Time{
self.time
}
pub fn collect(&mut self,instruction:Option<TimedInstruction<I>>){
@ -50,4 +50,4 @@ impl<I> InstructionCollector<I>{
None=>None,
}
}
}
}

@ -1,12 +1,16 @@
pub mod bvh;
pub mod map;
pub mod run;
pub mod aabb;
pub mod model;
pub mod mouse;
pub mod timer;
pub mod zeroes;
pub mod integer;
pub mod physics;
pub mod updatable;
pub mod instruction;
pub mod gameplay_attributes;
pub mod gameplay_modes;
pub mod gameplay_style;
pub mod controls_bitflag;
pub mod controls_bitflag;

26
src/mouse.rs Normal file

@ -0,0 +1,26 @@
use crate::integer::Time;
#[derive(Clone,Debug)]
pub struct MouseState{
pub pos:glam::IVec2,
pub time:Time,
}
impl Default for MouseState{
fn default()->Self{
Self{
time:Time::ZERO,
pos:glam::IVec2::ZERO,
}
}
}
impl MouseState{
pub fn lerp(&self,target:&MouseState,time:Time)->glam::IVec2{
let m0=self.pos.as_i64vec2();
let m1=target.pos.as_i64vec2();
//these are deltas
let t1t=(target.time-time).nanos();
let tt0=(time-self.time).nanos();
let dt=(target.time-self.time).nanos();
((m0*t1t+m1*tt0)/dt).as_ivec2()
}
}

27
src/physics.rs Normal file

@ -0,0 +1,27 @@
#[derive(Clone,Debug)]
pub enum Instruction{
ReplaceMouse(crate::mouse::MouseState,crate::mouse::MouseState),
SetNextMouse(crate::mouse::MouseState),
SetMoveRight(bool),
SetMoveUp(bool),
SetMoveBack(bool),
SetMoveLeft(bool),
SetMoveDown(bool),
SetMoveForward(bool),
SetJump(bool),
SetZoom(bool),
/// Reset: fully replace the physics state.
/// This forgets all inputs and settings which need to be reapplied.
Reset,
/// Restart: Teleport to the start zone.
Restart,
/// Spawn: Teleport to a specific mode's spawn
/// Sets current mode & spawn
Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
Idle,
//Idle: there were no input events, but the simulation is safe to advance to this timestep
//for interpolation / networking / playback reasons, most playback heads will always want
//to be 1 instruction ahead to generate the next state for interpolation.
PracticeFly,
SetSensitivity(crate::integer::Ratio64Vec2),
}

103
src/run.rs Normal file

@ -0,0 +1,103 @@
use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
use crate::integer::Time;
#[derive(Clone,Debug)]
pub enum FlagReason{
Anticheat,
StyleChange,
Clock,
Pause,
Flying,
Gravity,
Timescale,
TimeTravel,
Teleport,
}
impl ToString for FlagReason{
fn to_string(&self)->String{
match self{
FlagReason::Anticheat=>"Passed through anticheat zone.",
FlagReason::StyleChange=>"Changed style.",
FlagReason::Clock=>"Incorrect clock. (This can be caused by internet hiccups)",
FlagReason::Pause=>"Pausing is not allowed in this style.",
FlagReason::Flying=>"Flying is not allowed in this style.",
FlagReason::Gravity=>"Gravity modification is not allowed in this style.",
FlagReason::Timescale=>"Timescale is not allowed in this style.",
FlagReason::TimeTravel=>"Time travel is not allowed in this style.",
FlagReason::Teleport=>"Illegal teleport.",
}.to_owned()
}
}
#[derive(Debug)]
pub enum Error{
NotStarted,
AlreadyStarted,
AlreadyFinished,
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for Error{}
#[derive(Clone,Debug)]
enum RunState{
Created,
Started{timer:TimerFixed<Realtime,Unpaused>},
Finished{timer:TimerFixed<Realtime,Paused>},
}
#[derive(Clone,Debug)]
pub struct Run{
state:RunState,
flagged:Option<FlagReason>,
}
impl Run{
pub fn new()->Self{
Self{
state:RunState::Created,
flagged:None,
}
}
pub fn time(&self,time:Time)->Time{
match &self.state{
RunState::Created=>Time::ZERO,
RunState::Started{timer}=>timer.time(time),
RunState::Finished{timer}=>timer.time(time),
}
}
pub fn start(&mut self,time:Time)->Result<(),Error>{
match &self.state{
RunState::Created=>{
self.state=RunState::Started{
timer:TimerFixed::new(time,Time::ZERO),
};
Ok(())
},
RunState::Started{..}=>Err(Error::AlreadyStarted),
RunState::Finished{..}=>Err(Error::AlreadyFinished),
}
}
pub fn finish(&mut self,time:Time)->Result<(),Error>{
//this uses Copy
match &self.state{
RunState::Created=>Err(Error::NotStarted),
RunState::Started{timer}=>{
self.state=RunState::Finished{
timer:timer.into_paused(time),
};
Ok(())
},
RunState::Finished{..}=>Err(Error::AlreadyFinished),
}
}
pub fn flag(&mut self,flag_reason:FlagReason){
//don't replace the first reason the run was flagged
if self.flagged.is_none(){
self.flagged=Some(flag_reason);
}
}
}

319
src/timer.rs Normal file

@ -0,0 +1,319 @@
use crate::integer::{Time,Ratio64};
#[derive(Clone,Copy,Debug)]
pub struct Paused;
#[derive(Clone,Copy,Debug)]
pub struct Unpaused;
pub trait PauseState:Copy+std::fmt::Debug{
const IS_PAUSED:bool;
fn new()->Self;
}
impl PauseState for Paused{
const IS_PAUSED:bool=true;
fn new()->Self{
Self
}
}
impl PauseState for Unpaused{
const IS_PAUSED:bool=false;
fn new()->Self{
Self
}
}
#[derive(Clone,Copy,Debug)]
pub struct Realtime{
offset:Time,
}
impl Realtime{
pub const fn new(offset:Time)->Self{
Self{offset}
}
}
#[derive(Clone,Copy,Debug)]
pub struct Scaled{
scale:Ratio64,
offset:Time,
}
impl Scaled{
pub const fn new(scale:Ratio64,offset:Time)->Self{
Self{scale,offset}
}
const fn with_scale(scale:Ratio64)->Self{
Self{scale,offset:Time::ZERO}
}
const fn scale(&self,time:Time)->Time{
Time::raw(self.scale.mul_int(time.get()))
}
const fn get_scale(&self)->Ratio64{
self.scale
}
fn set_scale(&mut self,time:Time,new_scale:Ratio64){
let new_time=self.get_time(time);
self.scale=new_scale;
self.set_time(time,new_time);
}
}
pub trait TimerState:Copy+std::fmt::Debug{
fn identity()->Self;
fn get_time(&self,time:Time)->Time;
fn set_time(&mut self,time:Time,new_time:Time);
fn get_offset(&self)->Time;
fn set_offset(&mut self,offset:Time);
}
impl TimerState for Realtime{
fn identity()->Self{
Self{offset:Time::ZERO}
}
fn get_time(&self,time:Time)->Time{
time+self.offset
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-time;
}
fn get_offset(&self)->Time{
self.offset
}
fn set_offset(&mut self,offset:Time){
self.offset=offset;
}
}
impl TimerState for Scaled{
fn identity()->Self{
Self{scale:Ratio64::ONE,offset:Time::ZERO}
}
fn get_time(&self,time:Time)->Time{
self.scale(time)+self.offset
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-self.scale(time);
}
fn get_offset(&self)->Time{
self.offset
}
fn set_offset(&mut self,offset:Time){
self.offset=offset;
}
}
#[derive(Clone,Copy,Debug)]
pub struct TimerFixed<T:TimerState,P:PauseState>{
state:T,
_paused:P,
}
//scaled timer methods are generic across PauseState
impl<P:PauseState> TimerFixed<Scaled,P>{
pub fn scaled(time:Time,new_time:Time,scale:Ratio64)->Self{
let mut timer=Self{
state:Scaled::with_scale(scale),
_paused:P::new(),
};
timer.set_time(time,new_time);
timer
}
pub const fn get_scale(&self)->Ratio64{
self.state.get_scale()
}
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
self.state.set_scale(time,new_scale)
}
}
//pause and unpause is generic across TimerState
impl<T:TimerState> TimerFixed<T,Paused>{
pub fn into_unpaused(self,time:Time)->TimerFixed<T,Unpaused>{
let new_time=self.time(time);
let mut timer=TimerFixed{
state:self.state,
_paused:Unpaused,
};
timer.set_time(time,new_time);
timer
}
}
impl<T:TimerState> TimerFixed<T,Unpaused>{
pub fn into_paused(self,time:Time)->TimerFixed<T,Paused>{
let new_time=self.time(time);
let mut timer=TimerFixed{
state:self.state,
_paused:Paused,
};
timer.set_time(time,new_time);
timer
}
}
//the new constructor and time queries are generic across both
impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn new(time:Time,new_time:Time)->Self{
let mut timer=Self{
state:T::identity(),
_paused:P::new(),
};
timer.set_time(time,new_time);
timer
}
pub fn from_state(state:T)->Self{
Self{
state,
_paused:P::new(),
}
}
pub fn into_state(self)->T{
self.state
}
pub fn time(&self,time:Time)->Time{
match P::IS_PAUSED{
true=>self.state.get_offset(),
false=>self.state.get_time(time),
}
}
pub fn set_time(&mut self,time:Time,new_time:Time){
match P::IS_PAUSED{
true=>self.state.set_offset(new_time),
false=>self.state.set_time(time,new_time),
}
}
}
#[derive(Debug)]
pub enum Error{
AlreadyPaused,
AlreadyUnpaused,
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for Error{}
//wrapper type which holds type state internally
#[derive(Clone,Debug)]
pub enum Timer<T:TimerState>{
Paused(TimerFixed<T,Paused>),
Unpaused(TimerFixed<T,Unpaused>),
}
impl<T:TimerState> Timer<T>{
pub fn from_state(state:T,paused:bool)->Self{
match paused{
true=>Self::Paused(TimerFixed::from_state(state)),
false=>Self::Unpaused(TimerFixed::from_state(state)),
}
}
pub fn into_state(self)->(T,bool){
match self{
Self::Paused(timer)=>(timer.into_state(),true),
Self::Unpaused(timer)=>(timer.into_state(),false),
}
}
pub fn paused(time:Time,new_time:Time)->Self{
Self::Paused(TimerFixed::new(time,new_time))
}
pub fn unpaused(time:Time,new_time:Time)->Self{
Self::Unpaused(TimerFixed::new(time,new_time))
}
pub fn time(&self,time:Time)->Time{
match self{
Self::Paused(timer)=>timer.time(time),
Self::Unpaused(timer)=>timer.time(time),
}
}
pub fn set_time(&mut self,time:Time,new_time:Time){
match self{
Self::Paused(timer)=>timer.set_time(time,new_time),
Self::Unpaused(timer)=>timer.set_time(time,new_time),
}
}
pub fn pause(&mut self,time:Time)->Result<(),Error>{
*self=match *self{
Self::Paused(_)=>return Err(Error::AlreadyPaused),
Self::Unpaused(timer)=>Self::Paused(timer.into_paused(time)),
};
Ok(())
}
pub fn unpause(&mut self,time:Time)->Result<(),Error>{
*self=match *self{
Self::Paused(timer)=>Self::Unpaused(timer.into_unpaused(time)),
Self::Unpaused(_)=>return Err(Error::AlreadyUnpaused),
};
Ok(())
}
pub fn is_paused(&self)->bool{
match self{
Self::Paused(_)=>true,
Self::Unpaused(_)=>false,
}
}
pub fn set_paused(&mut self,time:Time,paused:bool)->Result<(),Error>{
match paused{
true=>self.pause(time),
false=>self.unpause(time),
}
}
}
//scaled timer methods are generic across PauseState
impl Timer<Scaled>{
pub const fn get_scale(&self)->Ratio64{
match self{
Self::Paused(timer)=>timer.get_scale(),
Self::Unpaused(timer)=>timer.get_scale(),
}
}
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
match self{
Self::Paused(timer)=>timer.set_scale(time,new_scale),
Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
}
}
}
#[cfg(test)]
mod test{
use super::*;
macro_rules! sec {
($s: expr) => {
Time::from_secs($s)
};
}
#[test]
fn test_timerfixed_scaled(){
//create a paused timer that reads 0s
let timer=TimerFixed::<Scaled,Paused>::new(sec!(0),sec!(0));
//the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0));
//unpause it after one second
let timer=timer.into_unpaused(sec!(1));
//the timer at 6 seconds should read 5s
assert_eq!(timer.time(sec!(6)),sec!(5));
//pause the timer after 11 seconds
let timer=timer.into_paused(sec!(11));
//the paused timer at 20 seconds should read 10s
assert_eq!(timer.time(sec!(20)),sec!(10));
}
#[test]
fn test_timer()->Result<(),Error>{
//create a paused timer that reads 0s
let mut timer=Timer::<Realtime>::paused(sec!(0),sec!(0));
//the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0));
//unpause it after one second
timer.unpause(sec!(1))?;
//the timer at 6 seconds should read 5s
assert_eq!(timer.time(sec!(6)),sec!(5));
//pause the timer after 11 seconds
timer.pause(sec!(11))?;
//the paused timer at 20 seconds should read 10s
assert_eq!(timer.time(sec!(20)),sec!(10));
Ok(())
}
}

@ -1,40 +1,47 @@
//find roots of polynomials
use arrayvec::ArrayVec;
use crate::integer::Planar64;
fn one(z0:Planar64)->ArrayVec<Planar64,2>{
let mut zeroes=ArrayVec::new();
unsafe{zeroes.push_unchecked(z0)}
zeroes
}
#[inline]
pub fn zeroes2(a0:Planar64,a1:Planar64,a2:Planar64) -> Vec<Planar64>{
pub fn zeroes2(a0:Planar64,a1:Planar64,a2:Planar64)->ArrayVec<Planar64,2>{
if a2==Planar64::ZERO{
return zeroes1(a0, a1);
return zeroes1(a0,a1);
}
let radicand=a1.get() as i128*a1.get() as i128-a2.get() as i128*a0.get() as i128*4;
if 0<radicand {
if 0<radicand{
//start with f64 sqrt
//failure case: 2^63 < sqrt(2^127)
let planar_radicand=Planar64::raw(unsafe{(radicand as f64).sqrt().to_int_unchecked()});
//TODO: one or two newtons
//sort roots ascending and avoid taking the difference of large numbers
match (Planar64::ZERO<a2,Planar64::ZERO<a1){
(true, true )=>vec![(-a1-planar_radicand)/(a2*2),(a0*2)/(-a1-planar_radicand)],
(true, false)=>vec![(a0*2)/(-a1+planar_radicand),(-a1+planar_radicand)/(a2*2)],
(false,true )=>vec![(a0*2)/(-a1-planar_radicand),(-a1-planar_radicand)/(a2*2)],
(false,false)=>vec![(-a1+planar_radicand)/(a2*2),(a0*2)/(-a1+planar_radicand)],
(true, true )=>[(-a1-planar_radicand)/(a2*2),(a0*2)/(-a1-planar_radicand)].into(),
(true, false)=>[(a0*2)/(-a1+planar_radicand),(-a1+planar_radicand)/(a2*2)].into(),
(false,true )=>[(a0*2)/(-a1-planar_radicand),(-a1-planar_radicand)/(a2*2)].into(),
(false,false)=>[(-a1+planar_radicand)/(a2*2),(a0*2)/(-a1+planar_radicand)].into(),
}
} else if radicand==0 {
return vec![a1/(a2*-2)];
} else {
return vec![];
}else if radicand==0{
return one(a1/(a2*-2));
}else{
return ArrayVec::new_const();
}
}
#[inline]
pub fn zeroes1(a0:Planar64,a1:Planar64) -> Vec<Planar64> {
pub fn zeroes1(a0:Planar64,a1:Planar64)->ArrayVec<Planar64,2>{
if a1==Planar64::ZERO{
return vec![];
return ArrayVec::new_const();
}else{
let q=((-a0.get() as i128)<<32)/(a1.get() as i128);
if i64::MIN as i128<=q&&q<=i64::MAX as i128{
return vec![Planar64::raw(q as i64)];
return one(Planar64::raw(q as i64));
}else{
return vec![];
return ArrayVec::new_const();
}
}
}
}