Compare commits

..

86 Commits

Author SHA1 Message Date
f98ffe6e0b fix spin bug edge case 2025-01-15 00:48:03 -08:00
f9d4ca8370 remove single use function 2025-01-15 00:36:15 -08:00
fea5bf4398 low polling rate edge case 2025-01-15 00:29:45 -08:00
d393f9f187 change some function names 2025-01-15 00:26:40 -08:00
ada34237c9 fix timeout timestamp 2025-01-15 00:02:26 -08:00
292df72709 transpose next buffer state calculation 2025-01-14 23:49:31 -08:00
7476d7cdc7 the mouse spin bug 2025-01-14 23:49:31 -08:00
6138d70a6f unify timeout 😍 2025-01-14 23:49:31 -08:00
cac4d698c3 fix overall correctness 2025-01-14 23:23:50 -08:00
e0ea0d6119 remove holdover case 2025-01-14 23:00:46 -08:00
80a4431ee8 test mouse_interpolator 2025-01-14 23:00:46 -08:00
80424cf24c spawn on map change 2025-01-14 21:41:55 -08:00
c338826513 finish 2025-01-14 21:16:36 -08:00
a6a242175b rewrite enums again 2025-01-14 20:44:17 -08:00
08bd57ffe1 remove incorrect comment 2025-01-14 19:02:34 -08:00
0d9c6648e2 accumulate mouse_pos as float 2025-01-14 19:00:17 -08:00
405cba3549 discover ternary method on bool 2025-01-14 18:39:38 -08:00
38d8dc1302 InstructionCache 2025-01-14 18:26:59 -08:00
33ccefc411 pop_buffered_instruction can be accomplished with mem::replace 2025-01-14 03:44:23 -08:00
93277c042b make pain code smaller 2025-01-14 01:45:09 -08:00
90f6437817 wrong instruction 2025-01-14 01:36:50 -08:00
29f9d5298f work 2025-01-14 01:34:26 -08:00
b0489a3746 work work work 2025-01-13 23:59:16 -08:00
a8847d3632 ruin physics code 2025-01-13 23:55:42 -08:00
fb8c2a619a rename fields in MouseInstruction::ReplaceMouse 2025-01-13 23:51:13 -08:00
6898302fa5 move code to more relevant location 2025-01-13 23:23:58 -08:00
52bbaaddc7 don't mutate physics_timeline on the fly 2025-01-13 23:23:58 -08:00
a8581a2a4f don't reconstruct MouseState struct with noop 2025-01-13 23:23:58 -08:00
c6ff11dd3e use replace_with to replace the enum variant in-place without cloning 2025-01-13 23:11:43 -08:00
844c7a08e1 add replace_with dep 2025-01-13 22:46:16 -08:00
bd61d03c91 work 2025-01-13 22:32:26 -08:00
b58ebb2775 todos 2025-01-11 01:50:06 -08:00
9095215cad write pop_buffered_instruction 2025-01-11 01:38:45 -08:00
92c30c3b87 cool changes 2025-01-11 01:07:06 -08:00
1b35c96f6e cook a bit 2025-01-10 23:18:53 -08:00
47bf9f1af3 pain 2025-01-10 22:08:54 -08:00
719c702b95 actually need ReplaceMouse because of OS level issue
The operating system does not report the timestamp at which it checks that the mouse was not moving, so the mouse interpolation will necessarily be incorrect for up to 1 polling period.  The alternative is to guess / make up a timestamp, but I don't want to do this.
2025-01-10 22:01:02 -08:00
ceb2499ad2 delete ReplaceMouse instruction 2025-01-10 20:59:25 -08:00
fe43ce9df6 progress 2025-01-10 20:03:53 -08:00
1fcd18bc45 how does it work 2025-01-09 21:20:25 -08:00
e371f95a4b a 2025-01-09 21:14:17 -08:00
b02c1bc7b4 idk if dropinstruction is gonan work 2025-01-09 21:14:15 -08:00
89446a933a a 2025-01-09 20:48:11 -08:00
0a3d965bb6 work 2025-01-09 20:48:11 -08:00
b6206d52c8 work 2025-01-09 20:48:11 -08:00
498c628280 asd 2025-01-09 20:48:11 -08:00
273e915f67 no 2025-01-09 20:48:11 -08:00
5072e5d7a8 yeah 2025-01-09 20:48:11 -08:00
3f0e3e0d3c update mouse interpolator code 2025-01-09 20:48:11 -08:00
2e88ae0612 wip 2025-01-09 20:48:11 -08:00
4c216a5b28 wip 2025-01-09 20:48:11 -08:00
0dc462a3b1 comment infinite loop avoidance 2025-01-09 20:38:32 -08:00
ca003edbc3 reintroduce generics to Instruction traits 2025-01-09 20:11:00 -08:00
16abe23e97 push solve tweaks 2025-01-09 06:27:50 -08:00
67f8569178 push_solve infallible type signature 2025-01-09 05:56:11 -08:00
121c9c5258 resize immediately 2025-01-09 05:36:55 -08:00
411b997b87 use mold linker because it's faster 2025-01-09 05:36:55 -08:00
4d587b6c0b fixed_wide: clippy angry about derivable traits 2025-01-08 23:33:39 -08:00
6ff74f08ab roblox_emulator: deref returns correct type 2025-01-08 23:31:56 -08:00
08f419f931 rename crawl_fev to crawl 2025-01-08 21:09:52 -08:00
6066e82fd2 MeshQuery trait FEV associated types 2025-01-08 21:09:52 -08:00
ca8035cdfc fixed wide 2025-01-08 21:09:52 -08:00
ff5d954cfb push solve! 2025-01-08 18:17:40 -08:00
a967f31004 rename RawTime to AbsoluteTime 2025-01-08 01:15:52 -08:00
8ad5d28e51 impl AsRef<str> for FlagReason 2025-01-07 23:57:22 -08:00
ab05893813 A BUG HAS BEEN FOUND!!!! 2025-01-07 23:43:54 -08:00
2f7597146e fixup strafe client for strongly typed time 2025-01-07 23:43:54 -08:00
004e0d3776 common: session Time 2025-01-07 23:43:54 -08:00
120d8197b7 common 2025-01-07 23:43:54 -08:00
36ba73a892 timers 2025-01-07 23:43:54 -08:00
86cf7e74b1 typed Time 2025-01-07 22:36:58 -08:00
24787fede5 improve get_model_transform readability 2025-01-07 20:19:44 -08:00
3797408bc8 pull out named variables in checkpoint_check 2025-01-07 06:03:29 -08:00
47c9b77b00 style 2025-01-07 06:03:29 -08:00
479e657251 notes 2025-01-07 06:03:29 -08:00
63fbc94287 snf: demo file brainstorming 2025-01-06 23:14:08 -08:00
1318ae20ca snf: session file brainstorming 2025-01-06 23:14:08 -08:00
851d9c935d clear mode state in teleport_to_spawn 2025-01-06 21:50:43 -08:00
d0a190861c implement NoJump and jump_limit 2025-01-06 21:45:48 -08:00
4dca7fc369 try_increment_jump_count monolithic function 2025-01-06 21:45:48 -08:00
62dfe23539 prevent hitting side of spawn from updating current stage 2025-01-06 21:05:37 -08:00
3991cb5064 document unclear function 2025-01-06 21:05:37 -08:00
1dc2556d85 factor out immutable checkpoint_check logic 2025-01-06 21:05:37 -08:00
4f21985290 fix print grammar 2025-01-06 00:36:14 -08:00
ccce54c1a3 calculate title at compile time 2025-01-05 21:39:48 -08:00
02bb2d797c functions not needed 2025-01-05 03:46:15 -08:00
37 changed files with 1881 additions and 1142 deletions

@ -1,2 +1,6 @@
[registries.strafesnet]
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
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"]

68
Cargo.lock generated

@ -67,12 +67,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -367,20 +361,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "chrono"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
@ -801,29 +781,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core 0.52.0",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
@ -2062,6 +2019,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "replace_with"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690"
[[package]]
name = "rmp"
version = "0.8.14"
@ -2271,22 +2234,20 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
name = "strafe-client"
version = "0.10.5"
dependencies = [
"arrayvec",
"bytemuck",
"chrono",
"configparser",
"ddsfile",
"glam",
"id",
"parking_lot",
"pollster",
"replace_with",
"strafesnet_bsp_loader",
"strafesnet_common",
"strafesnet_deferred_loader",
"strafesnet_rbx_loader",
"strafesnet_snf",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"wgpu",
"winit",
]
@ -2938,7 +2899,7 @@ dependencies = [
"web-sys",
"wgpu-types",
"windows",
"windows-core 0.58.0",
"windows-core",
]
[[package]]
@ -2967,16 +2928,7 @@ version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
]

@ -18,13 +18,8 @@ bitflags::bitflags!{
const Use=1<<14;//Interact with object
const PrimaryAction=1<<15;//LBM/Shoot/Melee
const SecondaryAction=1<<16;//RMB/ADS/Block
const WASD=Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).bits();
const WASDQE=Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown).bits();
}
}
impl Controls{
pub const fn wasd()->Self{
Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight)
}
pub const fn wasdqe()->Self{
Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown)
}
}

@ -1,5 +1,5 @@
use crate::model;
use crate::integer::{Time,Planar64,Planar64Vec3};
use crate::integer::{AbsoluteTime,Planar64,Planar64Vec3};
//you have this effect while in contact
#[derive(Clone,Hash,Eq,PartialEq)]
@ -31,7 +31,7 @@ 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
AirTime(AbsoluteTime),//increase airtime, invariant across mass and gravity changes
Height(Planar64),//increase height, invariant across mass and gravity changes
}
impl Booster{
@ -57,13 +57,13 @@ pub enum TrajectoryChoice{
#[derive(Clone,Hash,Eq,PartialEq)]
pub enum SetTrajectory{
//Speed-type SetTrajectory
AirTime(Time),//air time (relative to gravity direction) is invariant across mass and gravity changes
AirTime(AbsoluteTime),//air time (relative to gravity direction) is invariant across mass and gravity changes
Height(Planar64),//boost height (relative to gravity direction) is invariant across mass and gravity changes
DotVelocity{direction:Planar64Vec3,dot:Planar64},//set your velocity in a specific direction without touching other directions
//Velocity-type SetTrajectory
TargetPointTime{//launch on a trajectory that will land at a target point in a set amount of time
target_point:Planar64Vec3,
time:Time,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
time:AbsoluteTime,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
},
TargetPointSpeed{//launch at a fixed speed and land at a target point
target_point:Planar64Vec3,

@ -110,6 +110,7 @@ impl Stage{
pub fn into_inner(self)->(HashMap<CheckpointId,ModelId>,HashSet<ModelId>){
(self.ordered_checkpoints,self.unordered_checkpoints)
}
/// Returns true if the stage has no checkpoints.
#[inline]
pub const fn is_empty(&self)->bool{
self.is_complete(0,0)

@ -1,7 +1,8 @@
const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16
use crate::integer::{int,vec3::int as int3,Time,Ratio64,Planar64,Planar64Vec3};
use crate::integer::{int,vec3::int as int3,AbsoluteTime,Ratio64,Planar64,Planar64Vec3};
use crate::controls_bitflag::Controls;
use crate::physics::Time as PhysicsTime;
#[derive(Clone,Debug)]
pub struct StyleModifiers{
@ -48,7 +49,7 @@ pub enum JumpCalculation{
#[derive(Clone,Debug)]
pub enum JumpImpulse{
Time(Time),//jump time is invariant across mass and gravity changes
Time(AbsoluteTime),//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),// :)
@ -199,8 +200,8 @@ impl ControlsActivation{
}
pub const fn full_3d()->Self{
Self{
controls_mask:Controls::wasdqe(),
controls_intersects:Controls::wasdqe(),
controls_mask:Controls::WASDQE,
controls_intersects:Controls::WASDQE,
controls_contains:Controls::empty(),
}
}
@ -208,8 +209,8 @@ impl ControlsActivation{
//Normal
pub const fn full_2d()->Self{
Self{
controls_mask:Controls::wasd(),
controls_intersects:Controls::wasd(),
controls_mask:Controls::WASD,
controls_intersects:Controls::WASD,
controls_contains:Controls::empty(),
}
}
@ -272,8 +273,8 @@ impl StrafeSettings{
false=>None,
}
}
pub fn next_tick(&self,time:Time)->Time{
Time::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
pub fn next_tick(&self,time:PhysicsTime)->PhysicsTime{
PhysicsTime::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
}
pub const fn activates(&self,controls:Controls)->bool{
self.enable.activates(controls)
@ -435,7 +436,7 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:None,
mv:int(3),
tick_rate:Ratio64::new(64,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(64,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Energy(int(512)),
@ -477,10 +478,10 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:None,
mv:int(27)/10,
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Time(Time::from_micros(715_588)),
impulse:JumpImpulse::Time(AbsoluteTime::from_micros(715_588)),
calculation:JumpCalculation::Max,
limit_minimum:true,
}),
@ -534,7 +535,7 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some(Planar64::raw(150<<28)*100),
mv:(Planar64::raw(30)*VALVE_SCALE).fix_1(),
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
@ -575,7 +576,7 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some((int(150)*66*VALVE_SCALE).fix_1()),
mv:(int(30)*VALVE_SCALE).fix_1(),
tick_rate:Ratio64::new(66,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),

@ -1,35 +1,93 @@
use crate::integer::Time;
#[derive(Debug)]
pub struct TimedInstruction<I>{
pub time:Time,
pub struct TimedInstruction<I,T>{
pub time:Time<T>,
pub instruction:I,
}
/// Ensure all emitted instructions are processed before consuming external instructions
pub trait InstructionEmitter<I>{
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<I>>;
type TimeInner;
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
}
/// Apply an atomic state update
pub trait InstructionConsumer<I>{
fn process_instruction(&mut self, instruction:TimedInstruction<I>);
type TimeInner;
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
}
/// If the object produces its own instructions, allow exhaustively feeding them back in
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>
where
Time<T>:Copy,
{
fn process_exhaustive(&mut self,time_limit:Time<T>){
while let Some(instruction)=self.next_instruction(time_limit){
self.process_instruction(instruction);
}
}
}
impl<I,T,X> InstructionFeedback<I,T> for X
where
Time<T>:Copy,
X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
{}
pub struct InstructionCache<S,I,T>{
instruction_machine:S,
cached_instruction:Option<TimedInstruction<I,T>>,
time_limit:Time<T>,
}
impl<S,I,T> InstructionCache<S,I,T>
where
Time<T>:Copy+Ord,
Option<TimedInstruction<I,T>>:Clone,
S:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>
{
pub fn new(
instruction_machine:S,
)->Self{
Self{
instruction_machine,
cached_instruction:None,
time_limit:Time::MIN,
}
}
pub fn next_instruction_cached(&mut self,time_limit:Time<T>)->Option<TimedInstruction<I,T>>{
if time_limit<self.time_limit{
return self.cached_instruction.clone();
}
let next_instruction=self.instruction_machine.next_instruction(time_limit);
self.cached_instruction=next_instruction.clone();
self.time_limit=time_limit;
next_instruction
}
pub fn process_instruction(&mut self,instruction:TimedInstruction<I,T>){
// invalidate cache
self.time_limit=Time::MIN;
self.instruction_machine.process_instruction(instruction);
}
}
//PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I>{
time:Time,
pub struct InstructionCollector<I,T>{
time:Time<T>,
instruction:Option<I>,
}
impl<I> InstructionCollector<I>{
pub const fn new(time:Time)->Self{
impl<I,T> InstructionCollector<I,T>
where Time<T>:Copy+PartialOrd,
{
pub const fn new(time:Time<T>)->Self{
Self{
time,
instruction:None
}
}
#[inline]
pub const fn time(&self)->Time{
pub const fn time(&self)->Time<T>{
self.time
}
pub fn collect(&mut self,instruction:Option<TimedInstruction<I>>){
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
match instruction{
Some(unwrap_instruction)=>{
if unwrap_instruction.time<self.time {
@ -40,7 +98,7 @@ impl<I> InstructionCollector<I>{
None=>(),
}
}
pub fn instruction(self)->Option<TimedInstruction<I>>{
pub fn instruction(self)->Option<TimedInstruction<I,T>>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
match self.instruction{
Some(instruction)=>Some(TimedInstruction{

@ -2,19 +2,25 @@ pub use fixed_wide::fixed::{Fixed,Fix};
pub use ratio_ops::ratio::{Ratio,Divide};
//integer units
/// specific example of a "default" time type
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub struct Time(i64);
impl Time{
pub const MIN:Self=Self(i64::MIN);
pub const MAX:Self=Self(i64::MAX);
pub const ZERO:Self=Self(0);
pub const ONE_SECOND:Self=Self(1_000_000_000);
pub const ONE_MILLISECOND:Self=Self(1_000_000);
pub const ONE_MICROSECOND:Self=Self(1_000);
pub const ONE_NANOSECOND:Self=Self(1);
pub enum TimeInner{}
pub type AbsoluteTime=Time<TimeInner>;
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub struct Time<T>(i64,core::marker::PhantomData<T>);
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 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);
pub const ONE_NANOSECOND:Self=Self::raw(1);
#[inline]
pub const fn raw(num:i64)->Self{
Self(num)
Self(num,core::marker::PhantomData)
}
#[inline]
pub const fn get(self)->i64{
@ -22,19 +28,19 @@ impl Time{
}
#[inline]
pub const fn from_secs(num:i64)->Self{
Self(Self::ONE_SECOND.0*num)
Self::raw(Self::ONE_SECOND.0*num)
}
#[inline]
pub const fn from_millis(num:i64)->Self{
Self(Self::ONE_MILLISECOND.0*num)
Self::raw(Self::ONE_MILLISECOND.0*num)
}
#[inline]
pub const fn from_micros(num:i64)->Self{
Self(Self::ONE_MICROSECOND.0*num)
Self::raw(Self::ONE_MICROSECOND.0*num)
}
#[inline]
pub const fn from_nanos(num:i64)->Self{
Self(Self::ONE_NANOSECOND.0*num)
Self::raw(Self::ONE_NANOSECOND.0*num)
}
//should I have checked subtraction? force all time variables to be positive?
#[inline]
@ -45,14 +51,18 @@ impl Time{
pub const fn to_ratio(self)->Ratio<Planar64,Planar64>{
Ratio::new(Planar64::raw(self.0),Planar64::raw(1_000_000_000))
}
}
impl From<Planar64> for Time{
#[inline]
fn from(value:Planar64)->Self{
Time((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
pub const fn coerce<U>(self)->Time<U>{
Time::raw(self.0)
}
}
impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
impl<T> From<Planar64> for Time<T>{
#[inline]
fn from(value:Planar64)->Self{
Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
}
}
impl<T,Num,Den,N1,T1> From<Ratio<Num,Den>> for Time<T>
where
Num:core::ops::Mul<Planar64,Output=N1>,
N1:Divide<Den,Output=T1>,
@ -60,34 +70,34 @@ impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
{
#[inline]
fn from(value:Ratio<Num,Den>)->Self{
Time((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
}
}
impl std::fmt::Display for Time{
impl<T> std::fmt::Display for Time<T>{
#[inline]
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
}
}
impl std::default::Default for Time{
impl<T> std::default::Default for Time<T>{
fn default()->Self{
Self(0)
Self::raw(0)
}
}
impl std::ops::Neg for Time{
type Output=Time;
impl<T> std::ops::Neg for Time<T>{
type Output=Self;
#[inline]
fn neg(self)->Self::Output {
Time(-self.0)
Self::raw(-self.0)
}
}
macro_rules! impl_time_additive_operator {
($trait:ty, $method:ident) => {
impl $trait for Time{
type Output=Time;
impl<T> $trait for Time<T>{
type Output=Self;
#[inline]
fn $method(self,rhs:Self)->Self::Output {
Time(self.0.$method(rhs.0))
Self::raw(self.0.$method(rhs.0))
}
}
};
@ -97,7 +107,7 @@ impl_time_additive_operator!(core::ops::Sub,sub);
impl_time_additive_operator!(core::ops::Rem,rem);
macro_rules! impl_time_additive_assign_operator {
($trait:ty, $method:ident) => {
impl $trait for Time{
impl<T> $trait for Time<T>{
#[inline]
fn $method(&mut self,rhs:Self){
self.0.$method(rhs.0)
@ -108,53 +118,58 @@ macro_rules! impl_time_additive_assign_operator {
impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
impl std::ops::Mul for Time{
impl<T> std::ops::Mul for Time<T>{
type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
#[inline]
fn mul(self,rhs:Self)->Self::Output{
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
}
}
impl std::ops::Div<i64> for Time{
type Output=Time;
impl<T> std::ops::Div<i64> for Time<T>{
type Output=Self;
#[inline]
fn div(self,rhs:i64)->Self::Output{
Time(self.0/rhs)
Self::raw(self.0/rhs)
}
}
impl std::ops::Mul<i64> for Time{
type Output=Time;
impl<T> std::ops::Mul<i64> for Time<T>{
type Output=Self;
#[inline]
fn mul(self,rhs:i64)->Self::Output{
Time(self.0*rhs)
Self::raw(self.0*rhs)
}
}
impl core::ops::Mul<Time> for Planar64{
impl<T> core::ops::Mul<Time<T>> for Planar64{
type Output=Ratio<Fixed<2,64>,Planar64>;
fn mul(self,rhs:Time)->Self::Output{
fn mul(self,rhs:Time<T>)->Self::Output{
Ratio::new(self*Fixed::raw(rhs.0),Planar64::raw(1_000_000_000))
}
}
#[test]
fn time_from_planar64(){
let a:Time=Planar64::from(1).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_from_ratio(){
let a:Time=Ratio::new(Planar64::from(1),Planar64::from(1)).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_squared(){
let a=Time::from_secs(2);
assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
}
#[test]
fn time_times_planar64(){
let a=Time::from_secs(2);
let b=Planar64::from(2);
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
#[cfg(test)]
mod test_time{
use super::*;
type Time=super::AbsoluteTime;
#[test]
fn time_from_planar64(){
let a:Time=Planar64::from(1).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_from_ratio(){
let a:Time=Ratio::new(Planar64::from(1),Planar64::from(1)).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_squared(){
let a=Time::from_secs(2);
assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
}
#[test]
fn time_times_planar64(){
let a=Time::from_secs(2);
let b=Planar64::from(2);
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
}
}
#[inline]

@ -7,6 +7,7 @@ pub mod mouse;
pub mod timer;
pub mod integer;
pub mod physics;
pub mod session;
pub mod updatable;
pub mod instruction;
pub mod gameplay_attributes;

@ -1,11 +1,11 @@
use crate::integer::Time;
#[derive(Clone,Debug)]
pub struct MouseState{
pub struct MouseState<T>{
pub pos:glam::IVec2,
pub time:Time,
pub time:Time<T>,
}
impl Default for MouseState{
impl<T> Default for MouseState<T>{
fn default()->Self{
Self{
time:Time::ZERO,
@ -13,8 +13,10 @@ impl Default for MouseState{
}
}
}
impl MouseState{
pub fn lerp(&self,target:&MouseState,time:Time)->glam::IVec2{
impl<T> MouseState<T>
where Time<T>:Copy,
{
pub fn lerp(&self,target:&MouseState<T>,time:Time<T>)->glam::IVec2{
let m0=self.pos.as_i64vec2();
let m1=target.pos.as_i64vec2();
//these are deltas

@ -1,7 +1,34 @@
use crate::mouse::MouseState;
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;
#[derive(Clone,Debug)]
pub enum Instruction{
ReplaceMouse(crate::mouse::MouseState,crate::mouse::MouseState),
SetNextMouse(crate::mouse::MouseState),
Mouse(MouseInstruction),
Other(OtherInstruction),
}
impl Instruction{
pub const IDLE:Self=Self::Other(OtherInstruction::Other(OtherOtherInstruction::Idle));
}
#[derive(Clone,Debug)]
pub enum OtherInstruction{
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Other(OtherOtherInstruction),
}
#[derive(Clone,Debug)]
pub enum MouseInstruction{
/// Replace the entire interpolation state to avoid dividing by zero when replacing twice
ReplaceMouse{
m0:MouseState<TimeInner>,
m1:MouseState<TimeInner>,
},
SetNextMouse(MouseState<TimeInner>),
}
#[derive(Clone,Debug)]
pub enum SetControlInstruction{
SetMoveRight(bool),
SetMoveUp(bool),
SetMoveBack(bool),
@ -10,6 +37,9 @@ pub enum Instruction{
SetMoveForward(bool),
SetJump(bool),
SetZoom(bool),
}
#[derive(Clone,Debug)]
pub enum ModeInstruction{
/// Reset: fully replace the physics state.
/// This forgets all inputs and settings which need to be reapplied.
Reset,
@ -18,10 +48,11 @@ pub enum Instruction{
/// Spawn: Teleport to a specific mode's spawn
/// Sets current mode & spawn
Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
}
#[derive(Clone,Debug)]
pub enum OtherOtherInstruction{
/// Idle: there were no input events, but the simulation is safe to advance to this timestep
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),
}

@ -1,5 +1,10 @@
use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
use crate::integer::Time;
use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime};
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;
#[derive(Clone,Copy,Debug)]
pub enum FlagReason{
@ -15,6 +20,11 @@ pub enum FlagReason{
}
impl ToString for FlagReason{
fn to_string(&self)->String{
self.as_ref().to_owned()
}
}
impl AsRef<str> for FlagReason{
fn as_ref(&self)->&str{
match self{
FlagReason::Anticheat=>"Passed through anticheat zone.",
FlagReason::StyleChange=>"Changed style.",
@ -25,7 +35,7 @@ impl ToString for FlagReason{
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()
}
}
}
@ -45,8 +55,8 @@ impl std::error::Error for Error{}
#[derive(Clone,Copy,Debug)]
enum RunState{
Created,
Started{timer:TimerFixed<Realtime,Unpaused>},
Finished{timer:TimerFixed<Realtime,Paused>},
Started{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Unpaused>},
Finished{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Paused>},
}
#[derive(Clone,Copy,Debug)]
@ -62,14 +72,14 @@ impl Run{
flagged:None,
}
}
pub fn time(&self,time:Time)->Time{
pub fn time(&self,time:PhysicsTime)->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>{
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
match &self.state{
RunState::Created=>{
self.state=RunState::Started{
@ -81,7 +91,7 @@ impl Run{
RunState::Finished{..}=>Err(Error::AlreadyFinished),
}
}
pub fn finish(&mut self,time:Time)->Result<(),Error>{
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
//this uses Copy
match &self.state{
RunState::Created=>Err(Error::NotStarted),

@ -0,0 +1,3 @@
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;

@ -22,79 +22,106 @@ impl PauseState for Unpaused{
}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Inner{}
type InnerTime=Time<Inner>;
#[derive(Clone,Copy,Debug)]
pub struct Realtime{
offset:Time,
pub struct Realtime<In,Out>{
offset:InnerTime,
_in:core::marker::PhantomData<In>,
_out:core::marker::PhantomData<Out>,
}
impl Realtime{
pub const fn new(offset:Time)->Self{
Self{offset}
impl<In,Out> Realtime<In,Out>{
pub const fn new(offset:InnerTime)->Self{
Self{
offset,
_in:core::marker::PhantomData,
_out:core::marker::PhantomData,
}
}
}
#[derive(Clone,Copy,Debug)]
pub struct Scaled{
pub struct Scaled<In,Out>{
scale:Ratio64,
offset:Time,
offset:InnerTime,
_in:core::marker::PhantomData<In>,
_out:core::marker::PhantomData<Out>,
}
impl Scaled{
pub const fn new(scale:Ratio64,offset:Time)->Self{
Self{scale,offset}
impl<In,Out> Scaled<In,Out>
where Time<In>:Copy,
{
pub const fn new(scale:Ratio64,offset:InnerTime)->Self{
Self{
scale,
offset,
_in:core::marker::PhantomData,
_out:core::marker::PhantomData,
}
}
const fn with_scale(scale:Ratio64)->Self{
Self{scale,offset:Time::ZERO}
Self::new(scale,InnerTime::ZERO)
}
const fn scale(&self,time:Time)->Time{
Time::raw(self.scale.mul_int(time.get()))
const fn scale(&self,time:Time<In>)->InnerTime{
InnerTime::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){
fn set_scale(&mut self,time:Time<In>,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{
pub trait TimerState{
type In;
type Out;
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);
fn get_time(&self,time:Time<Self::In>)->Time<Self::Out>;
fn set_time(&mut self,time:Time<Self::In>,new_time:Time<Self::Out>);
fn get_offset(&self)->InnerTime;
fn set_offset(&mut self,offset:InnerTime);
}
impl TimerState for Realtime{
impl<In,Out> TimerState for Realtime<In,Out>{
type In=In;
type Out=Out;
fn identity()->Self{
Self{offset:Time::ZERO}
Self::new(InnerTime::ZERO)
}
fn get_time(&self,time:Time)->Time{
time+self.offset
fn get_time(&self,time:Time<In>)->Time<Out>{
time.coerce()+self.offset.coerce()
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-time;
fn set_time(&mut self,time:Time<In>,new_time:Time<Out>){
self.offset=new_time.coerce()-time.coerce();
}
fn get_offset(&self)->Time{
fn get_offset(&self)->InnerTime{
self.offset
}
fn set_offset(&mut self,offset:Time){
fn set_offset(&mut self,offset:InnerTime){
self.offset=offset;
}
}
impl TimerState for Scaled{
impl<In,Out> TimerState for Scaled<In,Out>
where Time<In>:Copy,
{
type In=In;
type Out=Out;
fn identity()->Self{
Self{scale:Ratio64::ONE,offset:Time::ZERO}
Self::new(Ratio64::ONE,InnerTime::ZERO)
}
fn get_time(&self,time:Time)->Time{
self.scale(time)+self.offset
fn get_time(&self,time:Time<In>)->Time<Out>{
(self.scale(time)+self.offset).coerce()
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-self.scale(time);
fn set_time(&mut self,time:Time<In>,new_time:Time<Out>){
self.offset=new_time.coerce()-self.scale(time);
}
fn get_offset(&self)->Time{
fn get_offset(&self)->InnerTime{
self.offset
}
fn set_offset(&mut self,offset:Time){
fn set_offset(&mut self,offset:InnerTime){
self.offset=offset;
}
}
@ -106,8 +133,10 @@ pub struct TimerFixed<T:TimerState,P:PauseState>{
}
//scaled timer methods are generic across PauseState
impl<P:PauseState> TimerFixed<Scaled,P>{
pub fn scaled(time:Time,new_time:Time,scale:Ratio64)->Self{
impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
where Time<In>:Copy,
{
pub fn scaled(time:Time<In>,new_time:Time<Out>,scale:Ratio64)->Self{
let mut timer=Self{
state:Scaled::with_scale(scale),
_paused:P::new(),
@ -118,14 +147,16 @@ impl<P:PauseState> TimerFixed<Scaled,P>{
pub const fn get_scale(&self)->Ratio64{
self.state.get_scale()
}
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
pub fn set_scale(&mut self,time:Time<In>,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>{
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(time);
let mut timer=TimerFixed{
state:self.state,
@ -135,8 +166,10 @@ impl<T:TimerState> TimerFixed<T,Paused>{
timer
}
}
impl<T:TimerState> TimerFixed<T,Unpaused>{
pub fn into_paused(self,time:Time)->TimerFixed<T,Paused>{
impl<T:TimerState> TimerFixed<T,Unpaused>
where Time<T::In>:Copy,
{
pub fn into_paused(self,time:Time<T::In>)->TimerFixed<T,Paused>{
let new_time=self.time(time);
let mut timer=TimerFixed{
state:self.state,
@ -149,7 +182,7 @@ impl<T:TimerState> TimerFixed<T,Unpaused>{
//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{
pub fn new(time:Time<T::In>,new_time:Time<T::Out>)->Self{
let mut timer=Self{
state:T::identity(),
_paused:P::new(),
@ -166,15 +199,15 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn into_state(self)->T{
self.state
}
pub fn time(&self,time:Time)->Time{
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
match P::IS_PAUSED{
true=>self.state.get_offset(),
true=>self.state.get_offset().coerce(),
false=>self.state.get_time(time),
}
}
pub fn set_time(&mut self,time:Time,new_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),
true=>self.state.set_offset(new_time.coerce()),
false=>self.state.set_time(time,new_time),
}
}
@ -198,7 +231,11 @@ pub enum Timer<T:TimerState>{
Paused(TimerFixed<T,Paused>),
Unpaused(TimerFixed<T,Unpaused>),
}
impl<T:TimerState> Timer<T>{
impl<T:TimerState> Timer<T>
where
T:Copy,
Time<T::In>:Copy,
{
pub fn from_state(state:T,paused:bool)->Self{
match paused{
true=>Self::Paused(TimerFixed::from_state(state)),
@ -211,32 +248,32 @@ impl<T:TimerState> Timer<T>{
Self::Unpaused(timer)=>(timer.into_state(),false),
}
}
pub fn paused(time:Time,new_time:Time)->Self{
pub fn paused(time:Time<T::In>,new_time:Time<T::Out>)->Self{
Self::Paused(TimerFixed::new(time,new_time))
}
pub fn unpaused(time:Time,new_time:Time)->Self{
pub fn unpaused(time:Time<T::In>,new_time:Time<T::Out>)->Self{
Self::Unpaused(TimerFixed::new(time,new_time))
}
pub fn time(&self,time:Time)->Time{
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
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){
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
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>{
pub fn pause(&mut self,time:Time<T::In>)->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>{
pub fn unpause(&mut self,time:Time<T::In>)->Result<(),Error>{
*self=match *self{
Self::Paused(timer)=>Self::Unpaused(timer.into_unpaused(time)),
Self::Unpaused(_)=>return Err(Error::AlreadyUnpaused),
@ -249,7 +286,7 @@ impl<T:TimerState> Timer<T>{
Self::Unpaused(_)=>false,
}
}
pub fn set_paused(&mut self,time:Time,paused:bool)->Result<(),Error>{
pub fn set_paused(&mut self,time:Time<T::In>,paused:bool)->Result<(),Error>{
match paused{
true=>self.pause(time),
false=>self.unpause(time),
@ -257,14 +294,16 @@ impl<T:TimerState> Timer<T>{
}
}
//scaled timer methods are generic across PauseState
impl Timer<Scaled>{
impl<In,Out> Timer<Scaled<In,Out>>
where Time<In>:Copy,
{
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){
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
match self{
Self::Paused(timer)=>timer.set_scale(time,new_scale),
Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
@ -280,10 +319,15 @@ mod test{
Time::from_secs($s)
};
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Parent{}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Calculated{}
#[test]
fn test_timerfixed_scaled(){
//create a paused timer that reads 0s
let timer=TimerFixed::<Scaled,Paused>::from_state(Scaled{scale:0.5f32.try_into().unwrap(),offset:sec!(0)});
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!(1)),sec!(0));
@ -300,7 +344,7 @@ mod test{
#[test]
fn test_timer()->Result<(),Error>{
//create a paused timer that reads 0s
let mut timer=Timer::<Realtime>::paused(sec!(0),sec!(0));
let mut timer=Timer::<Realtime<Parent,Calculated>>::paused(sec!(0),sec!(0));
//the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0));

@ -1,6 +1,6 @@
use bnum::{BInt,cast::As};
#[derive(Clone,Copy,Debug,Default,Hash)]
#[derive(Clone,Copy,Debug,Default,Hash,PartialEq,PartialOrd,Ord)]
/// A Fixed point number for which multiply operations widen the bits in the output. (when the wide-mul feature is enabled)
/// N is the number of u64s to use
/// F is the number of fractional bits (always N*32 lol)
@ -87,12 +87,6 @@ impl_from!(
i8,i16,i32,i64,i128,isize
);
impl<const N:usize,const F:usize> PartialEq for Fixed<N,F>{
#[inline]
fn eq(&self,other:&Self)->bool{
self.bits.eq(&other.bits)
}
}
impl<const N:usize,const F:usize,T> PartialEq<T> for Fixed<N,F>
where
T:Copy,
@ -105,12 +99,6 @@ where
}
impl<const N:usize,const F:usize> Eq for Fixed<N,F>{}
impl<const N:usize,const F:usize> PartialOrd for Fixed<N,F>{
#[inline]
fn partial_cmp(&self,other:&Self)->Option<std::cmp::Ordering>{
self.bits.partial_cmp(&other.bits)
}
}
impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
where
T:Copy,
@ -121,12 +109,6 @@ impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
self.bits.partial_cmp(&other.into())
}
}
impl<const N:usize,const F:usize> Ord for Fixed<N,F>{
#[inline]
fn cmp(&self,other:&Self)->std::cmp::Ordering{
self.bits.cmp(&other.bits)
}
}
impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
type Output=Self;

@ -33,7 +33,7 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
// LMAO look at this function!
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
f(&mut *dom)
f(*dom)
}
fn coerce_float32(value:&mlua::Value)->Option<f32>{

@ -7,21 +7,29 @@ pub enum Error{
/*
BLOCK_DEMO_HEADER:
u128 map_resource_id
u64 map_header_block_id
u32 num_maps
for map_id in 0..num_maps{
i64 simulation_time
u128 map_resource_id
u64 map_header_block_id
}
u32 num_bots
for bot_id in 0..num_bots{
i64 simulation_time
u128 bot_resource_id
u64 bot_header_block_id
}
//map loading timeline
//bot loading timeline
how to do worldstate for deathrun!?
- this is done in the client, there is no worldstate in the demo file
*/
pub struct StreamableDemo<R:BinReaderExt>{
map:Box<crate::map::StreamableMap<R>>,
map:Vec<crate::map::StreamableMap<R>>,
bots:Vec<crate::bot::StreamableBot<R>>,
}
impl<R:BinReaderExt> StreamableDemo<R>{

2
lib/snf/src/session.rs Normal file

@ -0,0 +1,2 @@
// a session is a recording of the client's inputs
// which should deterministically recreate a bot or whatever the client did

@ -15,21 +15,19 @@ source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies]
arrayvec = "0.7.6"
bytemuck = { version = "1.13.1", features = ["derive"] }
chrono = "0.4.39"
configparser = "3.0.2"
ddsfile = "0.5.1"
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
parking_lot = "0.12.1"
pollster = "0.4.0"
replace_with = "0.1.7"
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true }
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
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 = "23.0.1"
winit = "0.30.7"

160
strafe-client/src/body.rs Normal file

@ -0,0 +1,160 @@
use strafesnet_common::aabb;
use strafesnet_common::integer::{self,vec3,Time,Planar64,Planar64Vec3};
#[derive(Clone,Copy,Debug,Hash)]
pub struct Body<T>{
pub position:Planar64Vec3,//I64 where 2^32 = 1 u
pub velocity:Planar64Vec3,//I64 where 2^32 = 1 u/s
pub acceleration:Planar64Vec3,//I64 where 2^32 = 1 u/s/s
pub time:Time<T>,//nanoseconds x xxxxD!
}
impl<T> std::ops::Neg for Body<T>{
type Output=Self;
fn neg(self)->Self::Output{
Self{
position:self.position,
velocity:-self.velocity,
acceleration:self.acceleration,
time:-self.time,
}
}
}
impl<T> Body<T>
where Time<T>:Copy,
{
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
Self{
position,
velocity,
acceleration,
time,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.position
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().fix_1())
}
pub fn advance_time(&mut self,time:Time<T>){
self.position=self.extrapolated_position(time);
self.velocity=self.extrapolated_velocity(time);
self.time=time;
}
pub fn extrapolated_position_ratio_dt<Num,Den,N1,D1,N2,N3,D2,N4,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
// Why?
// All of this can be removed with const generics because the type can be specified as
// Ratio<Fixed<N,NF>,Fixed<D,DF>>
// which is known to implement all the necessary traits
Num:Copy,
Den:Copy+core::ops::Mul<i64,Output=D1>,
D1:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<D1,Output=N2>,
N1:core::ops::Add<N2,Output=N3>,
Num:core::ops::Mul<N3,Output=N4>,
Den:core::ops::Mul<D1,Output=D2>,
D2:Copy,
Planar64:core::ops::Mul<D2,Output=N4>,
N4:integer::Divide<D2,Output=T1>,
T1:integer::Fix<Planar64>,
{
// a*dt^2/2 + v*dt + p
// (a*dt/2+v)*dt+p
(self.acceleration.map(|elem|dt*elem/2)+self.velocity).map(|elem|dt.mul_ratio(elem))
.map(|elem|elem.divide().fix())+self.position
}
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
{
// a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
}
pub fn advance_time_ratio_dt(&mut self,dt:crate::model_physics::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into();
}
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::ZERO{
if self.acceleration==vec3::ZERO{
None
}else{
Some(self.acceleration)
}
}else{
Some(self.velocity)
}
}
pub fn grow_aabb(&self,aabb:&mut aabb::Aabb,t0:Time<T>,t1:Time<T>){
aabb.grow(self.extrapolated_position(t0));
aabb.grow(self.extrapolated_position(t1));
//v+a*t==0
//goober code
if !self.acceleration.x.is_zero(){
let t=-self.velocity.x/self.acceleration.x;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
if !self.acceleration.y.is_zero(){
let t=-self.velocity.y/self.acceleration.y;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
if !self.acceleration.z.is_zero(){
let t=-self.velocity.z/self.acceleration.z;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
}
}
impl<T> std::fmt::Display for Body<T>{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"p({}) v({}) a({}) t({})",self.position,self.velocity,self.acceleration,self.time)
}
}
pub struct VirtualBody<'a,T>{
body0:&'a Body<T>,
body1:&'a Body<T>,
}
impl<T> VirtualBody<'_,T>
where Time<T>:Copy,
{
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_velocity(time)-self.body0.extrapolated_velocity(time)
}
pub fn acceleration(&self)->Planar64Vec3{
self.body1.acceleration-self.body0.acceleration
}
pub fn body(&self,time:Time<T>)->Body<T>{
Body::new(self.extrapolated_position(time),self.extrapolated_velocity(time),self.acceleration(),time)
}
}

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

@ -1,23 +1,34 @@
use crate::physics::Body;
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge,MinkowskiMesh,MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert};
use strafesnet_common::integer::{Time,Fixed,Ratio};
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
#[derive(Debug)]
enum Transition<F,E:DirectedEdge,V>{
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<F,E,V>,GigaTime),
Hit(F,GigaTime),
Next(FEV<M>,GigaTime),
Hit(M::Face,GigaTime),
}
type MinkowskiFEV=FEV<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
pub enum CrawlResult<M:MeshQuery>{
Miss(FEV<M>),
Hit(M::Face,GigaTime),
}
fn next_transition(fev:&MinkowskiFEV,body_time:GigaTime,mesh:&MinkowskiMesh,body:&Body,mut best_time:GigaTime)->MinkowskiTransition{
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
//conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=MinkowskiTransition::Miss;
match fev{
&MinkowskiFEV::Face(face_id)=>{
let mut best_transition=Transition::Miss;
match self{
&FEV::Face(face_id)=>{
//test own face collision time, ignoring roots with zero or conflicting derivative
//n=face.normal d=face.dot
//n.a t^2+n.v t+n.p-d==0
@ -27,7 +38,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=MinkowskiTransition::Hit(face_id,dt);
best_transition=Transition::Hit(face_id,dt);
break;
}
}
@ -41,14 +52,14 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(verts[0])+mesh.vert(verts[1]))).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Edge(directed_edge_id.as_undirected()),dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
}
}
//if none:
},
&MinkowskiFEV::Edge(edge_id)=>{
&FEV::Edge(edge_id)=>{
//test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id);
@ -61,7 +72,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Face(edge_face_id),dt);
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break;
}
}
@ -74,14 +85,14 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Vert(vert_id),dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
}
}
}
//if none:
},
&MinkowskiFEV::Vert(vert_id)=>{
&FEV::Vert(vert_id)=>{
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.vert_edges(vert_id).iter(){
//edge is directed away from vertex, but we want the dot product to turn out negative
@ -90,7 +101,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Edge(directed_edge_id.as_undirected()),dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
}
@ -100,28 +111,24 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
}
best_transition
}
pub enum CrawlResult<F,E:DirectedEdge,V>{
Miss(FEV<F,E,V>),
Hit(F,GigaTime),
}
type MinkowskiCrawlResult=CrawlResult<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
pub fn crawl_fev(mut fev:MinkowskiFEV,mesh:&MinkowskiMesh,relative_body:&Body,start_time:Time,time_limit:Time)->MinkowskiCrawlResult{
let mut body_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
for _ in 0..20{
match next_transition(&fev,body_time,mesh,relative_body,time_limit){
Transition::Miss=>return CrawlResult::Miss(fev),
Transition::Next(next_fev,next_time)=>(fev,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
let mut body_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
}
}
//TODO: fix all bugs
//println!("Too many iterations! Using default behaviour instead of crashing...");
CrawlResult::Miss(self)
}
//TODO: fix all bugs
//println!("Too many iterations! Using default behaviour instead of crashing...");
CrawlResult::Miss(fev)
}

@ -1,7 +1,6 @@
use std::borrow::Cow;
use std::collections::{HashSet,HashMap};
use strafesnet_common::map;
use strafesnet_common::integer;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
use crate::model_graphics::{self,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
@ -98,12 +97,6 @@ impl std::default::Default for GraphicsCamera{
}
}
pub struct FrameState{
pub body:crate::physics::Body,
pub camera:crate::physics::PhysicsCamera,
pub time:integer::Time,
}
pub struct GraphicsState{
pipelines:GraphicsPipelines,
bind_groups:GraphicsBindGroups,
@ -882,7 +875,7 @@ impl GraphicsState{
view:&wgpu::TextureView,
device:&wgpu::Device,
queue:&wgpu::Queue,
frame_state:FrameState,
frame_state:crate::session::FrameState,
){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input

@ -1,5 +1,5 @@
pub enum Instruction{
Render(crate::graphics::FrameState),
Render(crate::session::FrameState),
//UpdateModel(crate::graphics::GraphicsModelUpdate),
Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings),
ChangeMap(strafesnet_common::map::CompleteMap),
@ -14,17 +14,13 @@ WorkerDescription{
*/
//up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order
fn print(message:&str){
web_sys::console::log_1(&message.into());
}
pub fn new<'a>(
pub fn new(
mut graphics:crate::graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface<'a>,
surface:wgpu::Surface,
device:wgpu::Device,
queue:wgpu::Queue,
)->crate::compat_worker::INWorker<'a,Instruction>{
let mut resize=None;
)->crate::compat_worker::INWorker<'_,Instruction>{
crate::compat_worker::INWorker::new(move |ins:Instruction|{
match ins{
Instruction::ChangeMap(map)=>{
@ -32,26 +28,15 @@ pub fn new<'a>(
graphics.generate_models(&device,&queue,&map);
},
Instruction::Resize(size,user_settings)=>{
resize=Some((size,user_settings));
println!("Resizing to {:?}",size);
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());
}
Instruction::Render(frame_state)=>{
if let Some((size,user_settings))=resize.take(){
print(format!("Resizing to {:?}",size).as_str());
//let t0=std::time::Instant::now();
match size{
winit::dpi::PhysicalSize{width:2560,height:1440}=>{
config.width=size.width.clamp(1,2560);
config.height=size.height.clamp(1,1440);
},
_=>{
config.width=size.width.clamp(1,1280);
config.height=size.height.clamp(1,720);
}
}
surface.configure(&device,&config);
graphics.resize(&device,&config,&user_settings);
//println!("Resize took {:?}",t0.elapsed());
}
//this has to go deeper somehow
let frame=match surface.get_current_texture(){
Ok(frame)=>frame,

@ -1,21 +1,23 @@
mod body;
mod file;
mod setup;
mod window;
mod worker;
mod physics;
mod session;
mod graphics;
mod settings;
mod push_solve;
mod face_crawler;
mod compat_worker;
mod model_physics;
mod model_graphics;
mod physics_worker;
mod graphics_worker;
mod mouse_interpolator;
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));
fn main(){
let title=format!("Strafe Client v{}",env!("CARGO_PKG_VERSION"));
#[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));
setup::setup_and_start(TITLE);
}

@ -3,6 +3,9 @@ use std::collections::{HashSet,HashMap};
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
use strafesnet_common::physics::Time;
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
pub trait UndirectedEdge{
type DirectedEdge:Copy+DirectedEdge;
@ -51,10 +54,10 @@ impl DirectedEdge for SubmeshDirectedEdgeId{
//Vertex <-> Edge <-> Face -> Collide
#[derive(Debug)]
pub enum FEV<F,E:DirectedEdge,V>{
Face(F),
Edge(E::UndirectedEdge),
Vert(V),
pub enum FEV<M:MeshQuery>{
Face(M::Face),
Edge(<M::Edge as DirectedEdge>::UndirectedEdge),
Vert(M::Vert),
}
//use Unit32 #[repr(C)] for map files
@ -64,25 +67,28 @@ struct Face{
dot:Planar64,
}
struct Vert(Planar64Vec3);
pub trait MeshQuery<FACE:Clone,EDGE:Clone+DirectedEdge,VERT:Clone>{
pub trait MeshQuery{
type Face:Clone;
type Edge:Clone+DirectedEdge;
type Vert:Clone;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
fn edge_n(&self,edge_id:EDGE::UndirectedEdge)->Planar64Vec3{
fn edge_n(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Planar64Vec3{
let verts=self.edge_verts(edge_id);
self.vert(verts[1].clone())-self.vert(verts[0].clone())
}
fn directed_edge_n(&self,directed_edge_id:EDGE)->Planar64Vec3{
fn directed_edge_n(&self,directed_edge_id:Self::Edge)->Planar64Vec3{
let verts=self.edge_verts(directed_edge_id.as_undirected());
(self.vert(verts[1].clone())-self.vert(verts[0].clone()))*((directed_edge_id.parity() as i64)*2-1)
}
fn vert(&self,vert_id:VERT)->Planar64Vec3;
fn face_nd(&self,face_id:FACE)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:FACE)->Cow<Vec<EDGE>>;
fn edge_faces(&self,edge_id:EDGE::UndirectedEdge)->Cow<[FACE;2]>;
fn edge_verts(&self,edge_id:EDGE::UndirectedEdge)->Cow<[VERT;2]>;
fn vert_edges(&self,vert_id:VERT)->Cow<Vec<EDGE>>;
fn vert_faces(&self,vert_id:VERT)->Cow<Vec<FACE>>;
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:Self::Face)->Cow<Vec<Self::Edge>>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Vert;2]>;
fn vert_edges(&self,vert_id:Self::Vert)->Cow<Vec<Self::Edge>>;
fn vert_faces(&self,vert_id:Self::Vert)->Cow<Vec<Self::Face>>;
}
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
@ -421,7 +427,10 @@ pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology,
}
impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMeshView<'_>{
impl MeshQuery for PhysicsMeshView<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
type Normal=Planar64Vec3;
type Offset=Planar64;
fn face_nd(&self,face_id:SubmeshFaceId)->(Planar64Vec3,Planar64){
@ -495,7 +504,10 @@ impl TransformedMesh<'_>{
)
}
}
impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for TransformedMesh<'_>{
impl MeshQuery for TransformedMesh<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
type Normal=Vector3<Fixed<3,96>>;
type Offset=Fixed<4,128>;
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
@ -669,13 +681,13 @@ impl MinkowskiMesh<'_>{
}
}
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>{
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh>{
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Vert(vert_id),
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if the boundary is not crossable and we are on the wrong side
let edge_n=self.edge_n(edge_id);
@ -693,14 +705,14 @@ impl MinkowskiMesh<'_>{
//infinity_dir can always be treated as a velocity
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
//both faces cannot pass this condition, return early if one does.
return FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Face(face_id);
return FEV::Face(face_id);
}
}
FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Edge(edge_id)
FEV::Edge(edge_id)
},
}
}
fn closest_fev_not_inside(&self,mut infinity_body:crate::physics::Body)->Option<FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>>{
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
@ -708,24 +720,24 @@ 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?
match crate::face_crawler::crawl_fev(infinity_fev,self,&infinity_body,integer::Time::MIN/4,infinity_body.time){
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:&crate::physics::Body,time_limit:integer::Time)->Option<(MinkowskiFace,GigaTime)>{
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
match crate::face_crawler::crawl_fev(fev,self,relative_body,relative_body.time,time_limit){
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:&crate::physics::Body,time_limit:integer::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=crate::physics::Body::new(
let infinity_body=Body::new(
relative_body.extrapolated_position(time_limit),
-relative_body.extrapolated_velocity(time_limit),
relative_body.acceleration,
@ -733,13 +745,13 @@ impl MinkowskiMesh<'_>{
);
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
//continue backwards along the body parabola
match crate::face_crawler::crawl_fev(fev,self,&-relative_body.clone(),-time_limit,-relative_body.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:&crate::physics::Body,time_limit:integer::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 mut best_time={
@ -766,15 +778,15 @@ impl MinkowskiMesh<'_>{
}
best_edge.map(|e|(e.as_undirected(),best_time))
}
fn infinity_in(&self,infinity_body:crate::physics::Body)->Option<(MinkowskiFace,GigaTime)>{
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
match crate::face_crawler::crawl_fev(infinity_fev,self,&infinity_body,integer::Time::MIN/4,infinity_body.time){
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=crate::physics::Body::new(point,vec3::Y,vec3::ZERO,integer::Time::ZERO);
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
//movement must escape the mesh forwards and backwards in time,
//otherwise the point is not inside the mesh
self.infinity_in(infinity_body)
@ -784,9 +796,13 @@ impl MinkowskiMesh<'_>{
)
}
}
impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiMesh<'_>{
impl MeshQuery for MinkowskiMesh<'_>{
type Face=MinkowskiFace;
type Edge=MinkowskiDirectedEdge;
type Vert=MinkowskiVert;
type Normal=Vector3<Fixed<3,96>>;
type Offset=Fixed<4,128>;
// TODO: relative d
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{

@ -0,0 +1,245 @@
use strafesnet_common::mouse::MouseState;
use strafesnet_common::physics::{
Instruction as PhysicsInputInstruction,
TimeInner as PhysicsTimeInner,
Time as PhysicsTime,
MouseInstruction,
OtherInstruction,
};
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
type TimedPhysicsInstruction=TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>;
type TimedUnbufferedInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
type DoubleTimedUnbufferedInstruction=TimedInstruction<TimedUnbufferedInstruction,SessionTimeInner>;
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
/// To be fed into MouseInterpolator
#[derive(Clone,Debug)]
pub enum Instruction{
MoveMouse(glam::IVec2),
Other(OtherInstruction),
}
pub enum StepInstruction{
Pop,
Timeout,
}
#[derive(Clone,Debug)]
enum BufferState{
Unbuffered,
Initializing(SessionTime,MouseState<PhysicsTimeInner>),
Buffered(SessionTime,MouseState<PhysicsTimeInner>),
}
pub struct MouseInterpolator{
buffer_state:BufferState,
// double timestamped timeline?
buffer:std::collections::VecDeque<TimedPhysicsInstruction>,
output:std::collections::VecDeque<TimedPhysicsInstruction>,
}
// Maybe MouseInterpolator manipulation is better expressed using impls
// and called from Instruction trait impls in session
impl InstructionConsumer<TimedUnbufferedInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:DoubleTimedUnbufferedInstruction){
self.push_unbuffered_input(ins)
}
}
impl InstructionEmitter<StepInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
self.buffered_instruction_with_timeout(time_limit)
}
}
impl MouseInterpolator{
pub fn new()->MouseInterpolator{
MouseInterpolator{
buffer_state:BufferState::Unbuffered,
buffer:std::collections::VecDeque::new(),
output:std::collections::VecDeque::new(),
}
}
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
self.buffer.push_front(TimedInstruction{
time:ins.time,
instruction:PhysicsInputInstruction::Mouse(ins.instruction),
});
// flush buffer to output
if self.output.len()==0{
// swap buffers
core::mem::swap(&mut self.buffer,&mut self.output);
}else{
// append buffer contents to output
self.output.append(&mut self.buffer);
}
}
fn get_mouse_timedout_at(&self,time_limit:SessionTime)->Option<SessionTime>{
match &self.buffer_state{
BufferState::Unbuffered=>None,
BufferState::Initializing(time,_mouse_state)
|BufferState::Buffered(time,_mouse_state)=>{
let timeout=*time+MOUSE_TIMEOUT;
(timeout<time_limit).then_some(timeout)
}
}
}
fn timeout_mouse(&mut self,time:PhysicsTime){
let buffer_state=core::mem::replace(&mut self.buffer_state,BufferState::Unbuffered);
match buffer_state{
BufferState::Unbuffered=>(),
BufferState::Initializing(_time,mouse_state)=>{
// only a single mouse move was sent in 10ms, this is very much an edge case!
self.push_mouse_and_flush_buffer(TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::ReplaceMouse{
m1:MouseState{pos:mouse_state.pos,time},
m0:mouse_state,
},
});
}
BufferState::Buffered(_time,mouse_state)=>{
// convert to BufferState::Unbuffered
// use the first instruction which should be a mouse instruction
// to push a ReplaceMouse instruction
// duplicate the current mouse
self.push_mouse_and_flush_buffer(TimedInstruction{
// This should be simulation_timer.time(timeout)
// but the timer is not accessible from this scope
// and it's just here to say that the mouse isn't moving anyways.
// I think this is a divide by zero bug, two identical mouse_states will occupy the interpolation state
time:mouse_state.time,
instruction:MouseInstruction::SetNextMouse(MouseState{pos:mouse_state.pos,time}),
});
},
}
}
pub fn push_unbuffered_input(&mut self,ins:DoubleTimedUnbufferedInstruction){
// new input
// if there is zero instruction buffered, it means the mouse is not moving
// case 1: unbuffered
// no mouse event is buffered
// - ins is mouse event? change to buffered
// - ins other -> write to timeline
// case 2: buffered
// a mouse event is buffered, and exists within the last 10ms
// case 3: stop
// a mouse event is buffered, but no mouse events have transpired within 10ms
// push buffered mouse instruction and flush buffer to output
if self.get_mouse_timedout_at(ins.time).is_some(){
self.timeout_mouse(ins.instruction.time);
}
// replace_with allows the enum variant to safely be replaced from behind a mutable reference
let (ins_mouse,ins_other)=replace_with::replace_with_or_abort_and_return(&mut self.buffer_state,|buffer_state|{
match ins.instruction.instruction{
Instruction::MoveMouse(pos)=>{
let next_mouse_state=MouseState{pos,time:ins.instruction.time};
match buffer_state{
BufferState::Unbuffered=>{
((None,None),BufferState::Initializing(ins.time,next_mouse_state))
},
BufferState::Initializing(_time,mouse_state)=>{
let ins_mouse=TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::ReplaceMouse{
m0:mouse_state,
m1:next_mouse_state.clone(),
},
};
((Some(ins_mouse),None),BufferState::Buffered(ins.time,next_mouse_state))
},
BufferState::Buffered(_time,mouse_state)=>{
let ins_mouse=TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::SetNextMouse(next_mouse_state.clone()),
};
((Some(ins_mouse),None),BufferState::Buffered(ins.time,next_mouse_state))
},
}
},
Instruction::Other(other_instruction)=>((None,Some(TimedInstruction{
time:ins.instruction.time,
instruction:other_instruction,
})),buffer_state),
}
});
if let Some(ins)=ins_mouse{
self.push_mouse_and_flush_buffer(ins);
}
if let Some(ins)=ins_other{
let instruction=TimedInstruction{
time:ins.time,
instruction:PhysicsInputInstruction::Other(ins.instruction),
};
if matches!(self.buffer_state,BufferState::Unbuffered){
self.output.push_back(instruction);
}else{
self.buffer.push_back(instruction);
}
}
}
pub fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
match self.get_mouse_timedout_at(time_limit){
Some(timeout)=>Some(TimedInstruction{
time:timeout,
instruction:StepInstruction::Timeout,
}),
None=>(self.output.len()!=0).then_some(TimedInstruction{
// this timestamp should not matter
time:time_limit,
instruction:StepInstruction::Pop,
}),
}
}
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>{
match ins.instruction{
StepInstruction::Pop=>(),
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
}
self.output.pop_front()
}
}
#[cfg(test)]
mod test{
use super::*;
#[test]
fn test(){
let mut interpolator=MouseInterpolator::new();
let timer=strafesnet_common::timer::Timer::<strafesnet_common::timer::Scaled<SessionTimeInner,PhysicsTimeInner>>::unpaused(SessionTime::ZERO,PhysicsTime::from_secs(1000));
macro_rules! push{
($time:expr,$ins:expr)=>{
println!("in={:?}",$ins);
interpolator.push_unbuffered_input(TimedInstruction{
time:$time,
instruction:TimedInstruction{
time:timer.time($time),
instruction:$ins,
}
});
while let Some(ins)=interpolator.buffered_instruction_with_timeout($time){
let ins_retimed=TimedInstruction{
time:timer.time(ins.time),
instruction:ins.instruction,
};
let out=interpolator.pop_buffered_instruction(ins_retimed);
println!("out={out:?}");
}
};
}
// test each buffer_state transition
let mut t=SessionTime::ZERO;
push!(t,Instruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(5);
push!(t,Instruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(5);
push!(t,Instruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(1);
}
}

@ -5,55 +5,32 @@ use strafesnet_common::map;
use strafesnet_common::run;
use strafesnet_common::aabb;
use strafesnet_common::model::{MeshId,ModelId};
use strafesnet_common::mouse::MouseState;
use strafesnet_common::gameplay_attributes::{self,CollisionAttributesId};
use strafesnet_common::gameplay_modes::{self,StageId};
use strafesnet_common::gameplay_style::{self,StyleModifiers};
use strafesnet_common::controls_bitflag::Controls;
use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer,TimedInstruction};
use strafesnet_common::integer::{self,vec3,mat3,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2};
use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer,InstructionFeedback,TimedInstruction};
use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2};
pub use strafesnet_common::physics::{Time,TimeInner};
use gameplay::ModeState;
pub type Body=crate::body::Body<TimeInner>;
type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
//external influence
//this is how you influence the physics from outside
use strafesnet_common::physics::Instruction as PhysicsInputInstruction;
use strafesnet_common::physics::{Instruction,OtherInstruction,MouseInstruction,ModeInstruction,OtherOtherInstruction,SetControlInstruction};
//internal influence
//when the physics asks itself what happens next, this is how it's represented
#[derive(Debug)]
enum PhysicsInternalInstruction{
pub enum InternalInstruction{
CollisionStart(Collision,model_physics::GigaTime),
CollisionEnd(Collision,model_physics::GigaTime),
StrafeTick,
ReachWalkTargetVelocity,
// Water,
}
#[derive(Debug)]
enum PhysicsInstruction{
Internal(PhysicsInternalInstruction),
//InputInstructions conditionally activate RefreshWalkTarget
//(by doing what SetWalkTargetVelocity used to do and then flagging it)
Input(PhysicsInputInstruction),
}
#[derive(Clone,Copy,Debug,Hash)]
pub struct Body{
pub position:Planar64Vec3,//I64 where 2^32 = 1 u
pub velocity:Planar64Vec3,//I64 where 2^32 = 1 u/s
pub acceleration:Planar64Vec3,//I64 where 2^32 = 1 u/s/s
pub time:Time,//nanoseconds x xxxxD!
}
impl std::ops::Neg for Body{
type Output=Self;
fn neg(self)->Self::Output{
Self{
position:self.position,
velocity:-self.velocity,
acceleration:self.acceleration,
time:-self.time,
}
}
}
#[derive(Clone,Debug,Default)]
pub struct InputState{
@ -235,11 +212,11 @@ impl PhysicsModels{
}
fn get_model_transform(&self,model_id:ModelId)->Option<&PhysicsMeshTransform>{
//ModelId can possibly be a decoration
self.contact_models.get(&ContactModelId::new(model_id.get())).map_or_else(
||self.intersect_models.get(&IntersectModelId::new(model_id.get()))
match self.contact_models.get(&ContactModelId::new(model_id.get())){
Some(model)=>Some(&model.transform),
None=>self.intersect_models.get(&IntersectModelId::new(model_id.get()))
.map(|model|&model.transform),
|model|Some(&model.transform)
)
}
}
fn contact_model(&self,model_id:ContactModelId)->&ContactModel{
&self.contact_models[&model_id]
@ -326,6 +303,15 @@ impl std::default::Default for PhysicsCamera{
}
mod gameplay{
use super::{gameplay_modes,HashSet,HashMap,ModelId};
pub enum JumpIncrementResult{
Allowed,
ExceededLimit,
}
impl JumpIncrementResult{
pub const fn is_allowed(self)->bool{
matches!(self,JumpIncrementResult::Allowed)
}
}
#[derive(Clone,Debug)]
pub struct ModeState{
mode_id:gameplay_modes::ModeId,
@ -344,8 +330,14 @@ mod gameplay{
pub const fn get_next_ordered_checkpoint_id(&self)->gameplay_modes::CheckpointId{
self.next_ordered_checkpoint_id
}
pub fn get_jump_count(&self,model_id:ModelId)->Option<u32>{
self.jump_counts.get(&model_id).copied()
fn increment_jump_count(&mut self,model_id:ModelId)->u32{
*self.jump_counts.entry(model_id).and_modify(|c|*c+=1).or_insert(1)
}
pub fn try_increment_jump_count(&mut self,model_id:ModelId,jump_limit:Option<u8>)->JumpIncrementResult{
match jump_limit{
Some(jump_limit) if (jump_limit as u32)<self.increment_jump_count(model_id)=>JumpIncrementResult::ExceededLimit,
_=>JumpIncrementResult::Allowed,
}
}
pub const fn ordered_checkpoint_count(&self)->u32{
self.next_ordered_checkpoint_id.get()
@ -559,13 +551,13 @@ impl MoveState{
=>None,
}
}
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
//check if you have a valid walk state and create an instruction
match self{
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{
&TransientAcceleration::Reachable{acceleration:_,time}=>Some(TimedInstruction{
time,
instruction:PhysicsInternalInstruction::ReachWalkTargetVelocity
instruction:InternalInstruction::ReachWalkTargetVelocity
}),
TransientAcceleration::Unreachable{acceleration:_}
|TransientAcceleration::Reached
@ -575,7 +567,7 @@ impl MoveState{
TimedInstruction{
time:strafe.next_tick(time),
//only poll the physics if there is a before and after mouse event
instruction:PhysicsInternalInstruction::StrafeTick
instruction:InternalInstruction::StrafeTick
}
}),
MoveState::Water=>None,//TODO
@ -726,8 +718,8 @@ impl Collision{
}
#[derive(Clone,Debug,Default)]
struct TouchingState{
contacts:HashSet::<ContactCollision>,
intersects:HashSet::<IntersectCollision>,
contacts:HashSet<ContactCollision>,
intersects:HashSet<IntersectCollision>,
}
impl TouchingState{
fn clear(&mut self){
@ -766,27 +758,29 @@ impl TouchingState{
a
}
fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:&mut Planar64Vec3){
//TODO: trey push solve
for contact in &self.contacts{
let contacts=self.contacts.iter().map(|contact|{
let n=contact_normal(models,hitbox_mesh,contact);
let d=n.dot(*velocity);
if d.is_negative(){
*velocity-=(n*d/n.length_squared()).divide().fix_1();
crate::push_solve::Contact{
position:vec3::ZERO,
velocity:n,
normal:n,
}
}
}).collect();
*velocity=crate::push_solve::push_solve(&contacts,*velocity);
}
fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:&mut Planar64Vec3){
//TODO: trey push solve
for contact in &self.contacts{
let contacts=self.contacts.iter().map(|contact|{
let n=contact_normal(models,hitbox_mesh,contact);
let d=n.dot(*acceleration);
if d.is_negative(){
*acceleration-=(n*d/n.length_squared()).divide().fix_1();
crate::push_solve::Contact{
position:vec3::ZERO,
velocity:n,
normal:n,
}
}
}).collect();
*acceleration=crate::push_solve::push_solve(&contacts,*acceleration);
}
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<PhysicsInternalInstruction>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){
let relative_body=VirtualBody::relative(&Body::ZERO,body).body(time);
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);
@ -794,7 +788,7 @@ impl TouchingState{
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:PhysicsInternalInstruction::CollisionEnd(
instruction:InternalInstruction::CollisionEnd(
Collision::Contact(*contact),
time
),
@ -808,7 +802,7 @@ impl TouchingState{
collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{
TimedInstruction{
time:relative_body.time+time.into(),
instruction:PhysicsInternalInstruction::CollisionEnd(
instruction:InternalInstruction::CollisionEnd(
Collision::Intersect(*intersect),
time
),
@ -818,142 +812,6 @@ impl TouchingState{
}
}
impl Body{
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time)->Self{
Self{
position,
velocity,
acceleration,
time,
}
}
pub fn extrapolated_position(&self,time:Time)->Planar64Vec3{
let dt=time-self.time;
self.position
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
}
pub fn extrapolated_velocity(&self,time:Time)->Planar64Vec3{
let dt=time-self.time;
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().fix_1())
}
pub fn advance_time(&mut self,time:Time){
self.position=self.extrapolated_position(time);
self.velocity=self.extrapolated_velocity(time);
self.time=time;
}
pub fn extrapolated_position_ratio_dt<Num,Den,N1,D1,N2,N3,D2,N4,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
// Why?
// All of this can be removed with const generics because the type can be specified as
// Ratio<Fixed<N,NF>,Fixed<D,DF>>
// which is known to implement all the necessary traits
Num:Copy,
Den:Copy+core::ops::Mul<i64,Output=D1>,
D1:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<D1,Output=N2>,
N1:core::ops::Add<N2,Output=N3>,
Num:core::ops::Mul<N3,Output=N4>,
Den:core::ops::Mul<D1,Output=D2>,
D2:Copy,
Planar64:core::ops::Mul<D2,Output=N4>,
N4:integer::Divide<D2,Output=T1>,
T1:integer::Fix<Planar64>,
{
// a*dt^2/2 + v*dt + p
// (a*dt/2+v)*dt+p
(self.acceleration.map(|elem|dt*elem/2)+self.velocity).map(|elem|dt.mul_ratio(elem))
.map(|elem|elem.divide().fix())+self.position
}
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
{
// a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
}
pub fn advance_time_ratio_dt(&mut self,dt:model_physics::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into();
}
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::ZERO{
if self.acceleration==vec3::ZERO{
None
}else{
Some(self.acceleration)
}
}else{
Some(self.velocity)
}
}
pub fn grow_aabb(&self,aabb:&mut aabb::Aabb,t0:Time,t1:Time){
aabb.grow(self.extrapolated_position(t0));
aabb.grow(self.extrapolated_position(t1));
//v+a*t==0
//goober code
if !self.acceleration.x.is_zero(){
let t=-self.velocity.x/self.acceleration.x;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
if !self.acceleration.y.is_zero(){
let t=-self.velocity.y/self.acceleration.y;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
if !self.acceleration.z.is_zero(){
let t=-self.velocity.z/self.acceleration.z;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
}
}
impl std::fmt::Display for Body{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"p({}) v({}) a({}) t({})",self.position,self.velocity,self.acceleration,self.time)
}
}
struct VirtualBody<'a>{
body0:&'a Body,
body1:&'a Body,
}
impl VirtualBody<'_>{
const fn relative<'a>(body0:&'a Body,body1:&'a Body)->VirtualBody<'a>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1,
}
}
fn extrapolated_position(&self,time:Time)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
}
fn extrapolated_velocity(&self,time:Time)->Planar64Vec3{
self.body1.extrapolated_velocity(time)-self.body0.extrapolated_velocity(time)
}
fn acceleration(&self)->Planar64Vec3{
self.body1.acceleration-self.body0.acceleration
}
fn body(&self,time:Time)->Body{
Body::new(self.extrapolated_position(time),self.extrapolated_velocity(time),self.acceleration(),time)
}
}
#[derive(Clone,Debug)]
pub struct PhysicsState{
time:Time,
@ -1017,11 +875,9 @@ impl PhysicsState{
self.touching.clear();
}
fn reset_to_default(&mut self){
let mut new_state=Self::default();
new_state.camera.sensitivity=self.camera.sensitivity;
*self=new_state;
*self=Self::default();
}
fn next_move_instruction(&self)->Option<TimedInstruction<PhysicsInternalInstruction>>{
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
self.move_state.next_move_instruction(&self.style.strafe,self.time)
}
fn cull_velocity(&mut self,data:&PhysicsData,velocity:Planar64Vec3){
@ -1070,15 +926,24 @@ pub struct PhysicsContext{
state:PhysicsState,//this captures the entire state of the physics.
data:PhysicsData,//data currently loaded into memory which is needded for physics to run, but is not part of the state.
}
//the physics consumes the generic PhysicsInstruction, but can only emit the more narrow PhysicsInternalInstruction
impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsContext{
fn process_instruction(&mut self,ins:TimedInstruction<PhysicsInstruction>){
atomic_state_update(&mut self.state,&self.data,ins)
// the physics consumes both Instruction and PhysicsInternalInstruction,
// but can only emit PhysicsInternalInstruction
impl InstructionConsumer<InternalInstruction> for PhysicsContext{
type TimeInner=TimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,TimeInner>){
atomic_internal_instruction(&mut self.state,&self.data,ins)
}
}
impl instruction::InstructionEmitter<PhysicsInternalInstruction> for PhysicsContext{
//this little next instruction function can cache its return value and invalidate the cached value by watching the State.
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
impl InstructionConsumer<Instruction> for PhysicsContext{
type TimeInner=TimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,TimeInner>){
atomic_input_instruction(&mut self.state,&self.data,ins)
}
}
impl InstructionEmitter<InternalInstruction> for PhysicsContext{
type TimeInner=TimeInner;
//this little next instruction function could cache its return value and invalidate the cached value by watching the State.
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
next_instruction_internal(&self.state,&self.data,time_limit)
}
}
@ -1235,29 +1100,14 @@ impl PhysicsContext{
println!("Physics Objects: {}",model_count);
}
//tickless gaming
fn run_internal_exhaustive(&mut self,time_limit:Time){
//prepare is ommitted - everything is done via instructions.
while let Some(instruction)=self.next_instruction(time_limit){//collect
//process
self.process_instruction(TimedInstruction{
time:instruction.time,
instruction:PhysicsInstruction::Internal(instruction.instruction),
});
//write hash lol
}
}
pub fn run_input_instruction(&mut self,instruction:TimedInstruction<PhysicsInputInstruction>){
self.run_internal_exhaustive(instruction.time);
self.process_instruction(TimedInstruction{
time:instruction.time,
instruction:PhysicsInstruction::Input(instruction.instruction),
});
pub fn run_input_instruction(&mut self,instruction:TimedInstruction<Instruction,TimeInner>){
self.process_exhaustive(instruction.time);
self.process_instruction(instruction);
}
}
//this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
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);
@ -1279,12 +1129,13 @@ impl PhysicsContext{
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
let time=relative_body.time+dt.into();
if time<=state.time{None}else{Some((time,face,dt))}})
.map(|(time,face,dt)|
(state.time<time).then_some((time,face,dt))
}).map(|(time,face,dt)|
TimedInstruction{
time,
instruction:PhysicsInternalInstruction::CollisionStart(
instruction:InternalInstruction::CollisionStart(
Collision::new(convex_mesh_id,face),
dt
)
@ -1441,7 +1292,7 @@ enum TeleportToSpawnError{
NoModel,
}
fn teleport_to_spawn(
stage:&gameplay_modes::Stage,
spawn_model_id:ModelId,
move_state:&mut MoveState,
body:&mut Body,
touching:&mut TouchingState,
@ -1456,14 +1307,78 @@ fn teleport_to_spawn(
input_state:&InputState,
time:Time,
)->Result<(),TeleportToSpawnError>{
//jump count and checkpoints are always reset on teleport_to_spawn.
//Map makers are expected to use tools to prevent
//multi-boosting on JumpLimit boosters such as spawning into a SetVelocity
mode_state.clear();
const EPSILON:Planar64=Planar64::raw((1<<32)/16);
let transform=models.get_model_transform(stage.spawn()).ok_or(TeleportToSpawnError::NoModel)?;
let transform=models.get_model_transform(spawn_model_id).ok_or(TeleportToSpawnError::NoModel)?;
//TODO: transform.vertex.matrix3.col(1)+transform.vertex.translation
let point=transform.vertex.transform_point3(vec3::Y).fix_1()+Planar64Vec3::new([Planar64::ZERO,style.hitbox.halfsize.y+EPSILON,Planar64::ZERO]);
teleport(point,move_state,body,touching,run,mode_state,Some(mode),models,hitbox_mesh,bvh,style,camera,input_state,time);
Ok(())
}
struct CheckpointCheckOutcome{
set_stage:Option<StageId>,
teleport_to_model:Option<ModelId>,
}
// stage_element.touch_result(mode,mode_state)
fn checkpoint_check(
mode_state:&ModeState,
stage_element:&gameplay_modes::StageElement,
mode:&gameplay_modes::Mode,
)->CheckpointCheckOutcome{
let current_stage_id=mode_state.get_stage_id();
let target_stage_id=stage_element.stage_id();
if current_stage_id<target_stage_id{
//checkpoint check
//check if current stage is complete
if let Some(current_stage)=mode.get_stage(current_stage_id){
if !current_stage.is_complete(mode_state.ordered_checkpoint_count(),mode_state.unordered_checkpoint_count()){
return CheckpointCheckOutcome{
set_stage:None,
teleport_to_model:Some(current_stage.spawn()),
};
}
}
//check if all between stages have no checkpoints required to pass them
for stage_id in current_stage_id.get()+1..target_stage_id.get(){
let stage_id=StageId::new(stage_id);
//check if none of the between stages has checkpoints, if they do teleport back to that stage
match mode.get_stage(stage_id){
Some(stage)=>if !stage.is_empty(){
return CheckpointCheckOutcome{
set_stage:Some(stage_id),
teleport_to_model:Some(stage.spawn()),
};
},
//no such stage! set to last existing stage
None=>return CheckpointCheckOutcome{
set_stage:Some(StageId::new(stage_id.get()-1)),
teleport_to_model:None,
},
}
};
//notably you do not get teleported for touching ordered checkpoints in the wrong order within the same stage.
return CheckpointCheckOutcome{
set_stage:Some(target_stage_id),
teleport_to_model:None,
};
}else if stage_element.force(){
//forced stage_element will set the stage_id even if the stage has already been passed
return CheckpointCheckOutcome{
set_stage:Some(target_stage_id),
teleport_to_model:None,
};
}
CheckpointCheckOutcome{
set_stage:None,
teleport_to_model:None,
}
}
fn run_teleport_behaviour(
model_id:ModelId,
wormhole:Option<&gameplay_attributes::Wormhole>,
@ -1481,70 +1396,38 @@ fn run_teleport_behaviour(
input_state:&InputState,
time:Time,
){
//TODO: jump count and checkpoints are always reset on teleport.
//Map makers are expected to use tools to prevent
//multi-boosting on JumpLimit boosters such as spawning into a SetVelocity
if let Some(mode)=mode{
if let Some(stage_element)=mode.get_element(model_id){
if let Some(stage)=mode.get_stage(stage_element.stage_id()){
if mode_state.get_stage_id()<stage_element.stage_id(){
//checkpoint check
//check if current stage is complete
if let Some(current_stage)=mode.get_stage(mode_state.get_stage_id()){
if !current_stage.is_complete(mode_state.ordered_checkpoint_count(),mode_state.unordered_checkpoint_count()){
//do the stage checkpoints have to be reset?
let _=teleport_to_spawn(current_stage,move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
return;
}
if let Some(stage_element)=mode.get_element(model_id){
if let Some(stage)=mode.get_stage(stage_element.stage_id()){
let CheckpointCheckOutcome{set_stage,teleport_to_model}=checkpoint_check(mode_state,stage_element,mode);
if let Some(stage_id)=set_stage{
mode_state.set_stage_id(stage_id);
}
//check if all between stages have no checkpoints required to pass them
let mut loop_unbroken=true;
for stage_id in mode_state.get_stage_id().get()+1..stage_element.stage_id().get(){
let stage_id=StageId::new(stage_id);
//check if none of the between stages has checkpoints, if they do teleport back to that stage
match mode.get_stage(stage_id){
Some(stage)=>if !stage.is_empty(){
mode_state.set_stage_id(stage_id);
let _=teleport_to_spawn(stage,move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
return;
},
None=>{
//no such stage! set to last existing stage and break loop
mode_state.set_stage_id(StageId::new(stage_id.get()-1));
loop_unbroken=false;
break;
},
}
};
//notably you do not get teleported for touching ordered checkpoints in the wrong order within the same stage.
if loop_unbroken{
mode_state.set_stage_id(stage_element.stage_id());
}
}else if stage_element.force(){
//forced stage_element will set the stage_id even if the stage has already been passed
mode_state.set_stage_id(stage_element.stage_id());
}
match stage_element.behaviour(){
gameplay_modes::StageElementBehaviour::SpawnAt=>(),
gameplay_modes::StageElementBehaviour::Trigger
|gameplay_modes::StageElementBehaviour::Teleport=>if let Some(mode_state_stage)=mode.get_stage(mode_state.get_stage_id()){
//I guess this is correct behaviour when trying to teleport to a non-existent spawn but it's still weird
let _=teleport_to_spawn(mode_state_stage,move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
if let Some(model_id)=teleport_to_model{
let _=teleport_to_spawn(model_id,move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
return;
},
gameplay_modes::StageElementBehaviour::Platform=>(),
gameplay_modes::StageElementBehaviour::Check=>(),//this is to run the checkpoint check behaviour without any other side effects
gameplay_modes::StageElementBehaviour::Checkpoint=>{
//each of these checks if the model is actually a valid respective checkpoint object
//accumulate sequential ordered checkpoints
mode_state.accumulate_ordered_checkpoint(&stage,model_id);
//insert model id in accumulated unordered checkpoints
mode_state.accumulate_unordered_checkpoint(&stage,model_id);
},
}
match stage_element.behaviour(){
gameplay_modes::StageElementBehaviour::SpawnAt=>(),
gameplay_modes::StageElementBehaviour::Trigger
|gameplay_modes::StageElementBehaviour::Teleport=>if let Some(mode_state_stage)=mode.get_stage(mode_state.get_stage_id()){
//I guess this is correct behaviour when trying to teleport to a non-existent spawn but it's still weird
let _=teleport_to_spawn(mode_state_stage.spawn(),move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
return;
},
gameplay_modes::StageElementBehaviour::Platform=>(),
gameplay_modes::StageElementBehaviour::Check=>(),//this is to run the checkpoint check behaviour without any other side effects
gameplay_modes::StageElementBehaviour::Checkpoint=>{
//each of these checks if the model is actually a valid respective checkpoint object
//accumulate sequential ordered checkpoints
mode_state.accumulate_ordered_checkpoint(&stage,model_id);
//insert model id in accumulated unordered checkpoints
mode_state.accumulate_unordered_checkpoint(&stage,model_id);
},
}
}
}
}
}
if let Some(&gameplay_attributes::Wormhole{destination_model})=wormhole{
if let (Some(origin),Some(destination))=(models.get_model_transform(model_id),models.get_model_transform(destination_model)){
let point=body.position-origin.vertex.translation+destination.vertex.translation;
@ -1554,6 +1437,18 @@ fn run_teleport_behaviour(
}
}
fn not_spawn_at(
mode:Option<&gameplay_modes::Mode>,
model_id:ModelId,
)->bool{
if let Some(mode)=mode{
if let Some(stage_element)=mode.get_element(model_id){
return stage_element.behaviour()!=gameplay_modes::StageElementBehaviour::SpawnAt;
}
}
true
}
fn collision_start_contact(
move_state:&mut MoveState,
body:&mut Body,
@ -1576,6 +1471,9 @@ fn collision_start_contact(
touching.insert(Collision::Contact(contact));
//clip v
set_velocity(body,touching,models,hitbox_mesh,incident_velocity);
let mut allow_jump=true;
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)=>println!("I'm surfing!"),
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
@ -1596,9 +1494,10 @@ fn collision_start_contact(
let walk_state=ContactMoveState::ladder(ladder_settings,body,gravity,target_velocity,contact);
move_state.set_move_state(MoveState::Ladder(walk_state),body,touching,models,hitbox_mesh,style,camera,input_state);
},
Some(gameplay_attributes::ContactingBehaviour::NoJump)=>todo!("nyi"),
Some(gameplay_attributes::ContactingBehaviour::NoJump)=>allow_jump=false,
None=>if let Some(walk_settings)=&style.walk{
if walk_settings.is_slope_walkable(contact_normal(models,hitbox_mesh,&contact),vec3::Y){
allow_run_teleport_behaviour=true;
//ground
let (gravity,target_velocity)=ground_things(walk_settings,&contact,touching,models,hitbox_mesh,style,camera,input_state);
let walk_state=ContactMoveState::ground(walk_settings,body,gravity,target_velocity,contact);
@ -1607,12 +1506,30 @@ fn collision_start_contact(
},
}
//I love making functions with 10 arguments to dodge the borrow checker
run_teleport_behaviour(contact.model_id.into(),attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time);
if style.get_control(Controls::Jump,input_state.controls){
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);
}
if allow_jump&&style.get_control(Controls::Jump,input_state.controls){
if let (Some(jump_settings),Some(walk_state))=(&style.jump,move_state.get_walk_state()){
let jump_dir=walk_state.jump_direction.direction(models,hitbox_mesh,&walk_state.contact);
let jumped_velocity=jump_settings.jumped_velocity(style,jump_dir,body.velocity,attr.general.booster.as_ref());
move_state.cull_velocity(jumped_velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
let mut exceeded_jump_limit=false;
if let Some(mode)=mode{
if let Some(stage_element)=mode.get_element(model_id){
if !mode_state.try_increment_jump_count(model_id,stage_element.jump_limit()).is_allowed(){
exceeded_jump_limit=true;
}
}
}
if exceeded_jump_limit{
if let Some(mode)=mode{
if let Some(spawn_model_id)=mode.get_spawn_model_id(mode_state.get_stage_id()){
let _=teleport_to_spawn(spawn_model_id,move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
}
}
}else{
let jump_dir=walk_state.jump_direction.direction(models,hitbox_mesh,&walk_state.contact);
let jumped_velocity=jump_settings.jumped_velocity(style,jump_dir,body.velocity,attr.general.booster.as_ref());
move_state.cull_velocity(jumped_velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
}
}
}
match &attr.general.trajectory{
@ -1724,13 +1641,13 @@ fn collision_end_intersect(
}
}
}
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInternalInstruction>){
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,TimeInner>){
state.time=ins.time;
let (should_advance_body,goober_time)=match ins.instruction{
PhysicsInternalInstruction::CollisionStart(_,dt)
|PhysicsInternalInstruction::CollisionEnd(_,dt)=>(true,Some(dt)),
PhysicsInternalInstruction::StrafeTick
|PhysicsInternalInstruction::ReachWalkTargetVelocity=>(true,None),
InternalInstruction::CollisionStart(_,dt)
|InternalInstruction::CollisionEnd(_,dt)=>(true,Some(dt)),
InternalInstruction::StrafeTick
|InternalInstruction::ReachWalkTargetVelocity=>(true,None),
};
if should_advance_body{
match goober_time{
@ -1739,7 +1656,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
}
}
match ins.instruction{
PhysicsInternalInstruction::CollisionStart(collision,_)=>{
InternalInstruction::CollisionStart(collision,_)=>{
let mode=data.modes.get_mode(state.mode_state.get_mode_id());
match collision{
Collision::Contact(contact)=>collision_start_contact(
@ -1760,7 +1677,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
),
}
},
PhysicsInternalInstruction::CollisionEnd(collision,_)=>match collision{
InternalInstruction::CollisionEnd(collision,_)=>match collision{
Collision::Contact(contact)=>collision_end_contact(
&mut state.move_state,&mut state.body,&mut state.touching,&data.models,&data.hitbox_mesh,&state.style,&state.camera,&state.input_state,
data.models.contact_attr(contact.model_id),
@ -1775,7 +1692,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
state.time
),
},
PhysicsInternalInstruction::StrafeTick=>{
InternalInstruction::StrafeTick=>{
//TODO make this less huge
if let Some(strafe_settings)=&state.style.strafe{
let controls=state.input_state.controls;
@ -1793,7 +1710,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
}
}
}
PhysicsInternalInstruction::ReachWalkTargetVelocity=>{
InternalInstruction::ReachWalkTargetVelocity=>{
match &mut state.move_state{
MoveState::Air
|MoveState::Water
@ -1820,27 +1737,18 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
}
}
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInputInstruction>){
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,TimeInner>){
state.time=ins.time;
let should_advance_body=match ins.instruction{
//the body may as well be a quantum wave function
//as far as these instruction are concerned (they don't care where it is)
PhysicsInputInstruction::SetSensitivity(..)
|PhysicsInputInstruction::Reset
|PhysicsInputInstruction::Restart
|PhysicsInputInstruction::Spawn(..)
|PhysicsInputInstruction::SetZoom(..)
|PhysicsInputInstruction::Idle=>false,
Instruction::Other(OtherInstruction::Other(OtherOtherInstruction::SetSensitivity(..)))
|Instruction::Other(OtherInstruction::Mode(_))
|Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetZoom(..)))
|Instruction::Other(OtherInstruction::Other(OtherOtherInstruction::Idle))=>false,
//these controls only update the body if you are on the ground
PhysicsInputInstruction::SetNextMouse(..)
|PhysicsInputInstruction::ReplaceMouse(..)
|PhysicsInputInstruction::SetMoveForward(..)
|PhysicsInputInstruction::SetMoveLeft(..)
|PhysicsInputInstruction::SetMoveBack(..)
|PhysicsInputInstruction::SetMoveRight(..)
|PhysicsInputInstruction::SetMoveUp(..)
|PhysicsInputInstruction::SetMoveDown(..)
|PhysicsInputInstruction::SetJump(..)=>{
Instruction::Mouse(_)
|Instruction::Other(OtherInstruction::SetControl(_))=>{
match &state.move_state{
MoveState::Fly
|MoveState::Water
@ -1850,125 +1758,107 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
}
},
//the body must be updated unconditionally
PhysicsInputInstruction::PracticeFly=>true,
Instruction::Other(OtherInstruction::Other(OtherOtherInstruction::PracticeFly))=>true,
};
if should_advance_body{
state.body.advance_time(state.time);
}
//TODO: UNTAB
let mut b_refresh_walk_target=true;
match ins.instruction{
PhysicsInputInstruction::SetSensitivity(sensitivity)=>state.camera.sensitivity=sensitivity,
PhysicsInputInstruction::SetNextMouse(m)=>{
state.camera.move_mouse(state.input_state.mouse_delta());
state.input_state.set_next_mouse(m);
},
PhysicsInputInstruction::ReplaceMouse(m0,m1)=>{
state.camera.move_mouse(m0.pos-state.input_state.mouse.pos);
state.input_state.replace_mouse(m0,m1);
},
PhysicsInputInstruction::SetMoveForward(s)=>state.input_state.set_control(Controls::MoveForward,s),
PhysicsInputInstruction::SetMoveLeft(s)=>state.input_state.set_control(Controls::MoveLeft,s),
PhysicsInputInstruction::SetMoveBack(s)=>state.input_state.set_control(Controls::MoveBackward,s),
PhysicsInputInstruction::SetMoveRight(s)=>state.input_state.set_control(Controls::MoveRight,s),
PhysicsInputInstruction::SetMoveUp(s)=>state.input_state.set_control(Controls::MoveUp,s),
PhysicsInputInstruction::SetMoveDown(s)=>state.input_state.set_control(Controls::MoveDown,s),
PhysicsInputInstruction::SetJump(s)=>{
state.input_state.set_control(Controls::Jump,s);
if let Some(walk_state)=state.move_state.get_walk_state(){
if let Some(jump_settings)=&state.style.jump{
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);
}
}
b_refresh_walk_target=false;
},
PhysicsInputInstruction::SetZoom(s)=>{
state.input_state.set_control(Controls::Zoom,s);
b_refresh_walk_target=false;
},
PhysicsInputInstruction::Reset=>{
//totally reset physics state
state.reset_to_default();
b_refresh_walk_target=false;
},
PhysicsInputInstruction::Restart=>{
//teleport to start zone
let mode=data.modes.get_mode(state.mode_state.get_mode_id());
let spawn_point=mode.and_then(|mode|
//TODO: spawn at the bottom of the start zone plus the hitbox size
//TODO: set camera andles to face the same way as the start zone
data.models.get_model_transform(mode.get_start().into()).map(|transform|
transform.vertex.translation
)
).unwrap_or(vec3::ZERO);
set_position(spawn_point,&mut state.move_state,&mut state.body,&mut state.touching,&mut state.run,&mut state.mode_state,mode,&data.models,&data.hitbox_mesh,&data.bvh,&state.style,&state.camera,&state.input_state,state.time);
set_velocity(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
state.set_move_state(data,MoveState::Air);
b_refresh_walk_target=false;
}
PhysicsInputInstruction::Spawn(mode_id,stage_id)=>{
//spawn at a particular stage
if let Some(mode)=data.modes.get_mode(mode_id){
if let Some(stage)=mode.get_stage(stage_id){
let _=teleport_to_spawn(
stage,
&mut state.move_state,&mut state.body,&mut state.touching,&mut state.run,&mut state.mode_state,
mode,
&data.models,&data.hitbox_mesh,&data.bvh,&state.style,&state.camera,&state.input_state,state.time
);
}
}
b_refresh_walk_target=false;
},
PhysicsInputInstruction::PracticeFly=>{
match &state.move_state{
MoveState::Fly=>{
state.set_move_state(data,MoveState::Air);
},
_=>{
state.set_move_state(data,MoveState::Fly);
},
}
b_refresh_walk_target=false;
},
PhysicsInputInstruction::Idle=>{
//literally idle!
b_refresh_walk_target=false;
},
}
if b_refresh_walk_target{
state.apply_input_and_body(data);
state.cull_velocity(data,state.body.velocity);
//also check if accelerating away from surface
}
}
fn atomic_state_update(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInstruction>){
match &ins.instruction{
PhysicsInstruction::Input(PhysicsInputInstruction::Idle)
|PhysicsInstruction::Input(PhysicsInputInstruction::SetNextMouse(_))
|PhysicsInstruction::Input(PhysicsInputInstruction::ReplaceMouse(_,_))
|PhysicsInstruction::Internal(PhysicsInternalInstruction::StrafeTick)
|PhysicsInstruction::Internal(PhysicsInternalInstruction::ReachWalkTargetVelocity)=>(),
_=>println!("{}|{:?}",ins.time,ins.instruction),
}
if ins.time<state.time{
println!("@@@@ Time travel warning! state.time={} ins.time={}\nInstruction={:?}",state.time,ins.time,ins.instruction);
}
//idle is special, it is specifically a no-op to get Internal events to catch up to real time
match ins.instruction{
PhysicsInstruction::Input(PhysicsInputInstruction::Idle)=>(),
PhysicsInstruction::Internal(instruction)=>atomic_internal_instruction(state,data,TimedInstruction{time:ins.time,instruction}),
PhysicsInstruction::Input(instruction)=>atomic_input_instruction(state,data,TimedInstruction{time:ins.time,instruction}),
let mut b_refresh_walk_target=true;
match ins.instruction{
Instruction::Mouse(MouseInstruction::SetNextMouse(m))=>{
state.camera.move_mouse(state.input_state.mouse_delta());
state.input_state.set_next_mouse(m);
},
Instruction::Mouse(MouseInstruction::ReplaceMouse{m0,m1})=>{
state.camera.move_mouse(m0.pos-state.input_state.mouse.pos);
state.input_state.replace_mouse(m0,m1);
},
Instruction::Other(OtherInstruction::Other(OtherOtherInstruction::SetSensitivity(sensitivity)))=>state.camera.sensitivity=sensitivity,
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetMoveForward(s)))=>state.input_state.set_control(Controls::MoveForward,s),
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetMoveLeft(s)))=>state.input_state.set_control(Controls::MoveLeft,s),
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetMoveBack(s)))=>state.input_state.set_control(Controls::MoveBackward,s),
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetMoveRight(s)))=>state.input_state.set_control(Controls::MoveRight,s),
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetMoveUp(s)))=>state.input_state.set_control(Controls::MoveUp,s),
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetMoveDown(s)))=>state.input_state.set_control(Controls::MoveDown,s),
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetJump(s)))=>{
state.input_state.set_control(Controls::Jump,s);
if let Some(walk_state)=state.move_state.get_walk_state(){
if let Some(jump_settings)=&state.style.jump{
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);
}
}
b_refresh_walk_target=false;
},
Instruction::Other(OtherInstruction::SetControl(SetControlInstruction::SetZoom(s)))=>{
state.input_state.set_control(Controls::Zoom,s);
b_refresh_walk_target=false;
},
Instruction::Other(OtherInstruction::Mode(ModeInstruction::Reset))=>{
//totally reset physics state
state.reset_to_default();
b_refresh_walk_target=false;
},
Instruction::Other(OtherInstruction::Mode(ModeInstruction::Restart))=>{
//teleport to start zone
let mode=data.modes.get_mode(state.mode_state.get_mode_id());
let spawn_point=mode.and_then(|mode|
//TODO: spawn at the bottom of the start zone plus the hitbox size
//TODO: set camera andles to face the same way as the start zone
data.models.get_model_transform(mode.get_start().into()).map(|transform|
transform.vertex.translation
)
).unwrap_or(vec3::ZERO);
set_position(spawn_point,&mut state.move_state,&mut state.body,&mut state.touching,&mut state.run,&mut state.mode_state,mode,&data.models,&data.hitbox_mesh,&data.bvh,&state.style,&state.camera,&state.input_state,state.time);
set_velocity(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
state.set_move_state(data,MoveState::Air);
b_refresh_walk_target=false;
}
// Spawn does not necessarily imply reset
Instruction::Other(OtherInstruction::Mode(ModeInstruction::Spawn(mode_id,stage_id)))=>{
//spawn at a particular stage
if let Some(mode)=data.modes.get_mode(mode_id){
if let Some(stage)=mode.get_stage(stage_id){
let _=teleport_to_spawn(
stage.spawn(),
&mut state.move_state,&mut state.body,&mut state.touching,&mut state.run,&mut state.mode_state,
mode,
&data.models,&data.hitbox_mesh,&data.bvh,&state.style,&state.camera,&state.input_state,state.time
);
}
}
b_refresh_walk_target=false;
},
Instruction::Other(OtherInstruction::Other(OtherOtherInstruction::PracticeFly))=>{
match &state.move_state{
MoveState::Fly=>{
state.set_move_state(data,MoveState::Air);
},
_=>{
state.set_move_state(data,MoveState::Fly);
},
}
b_refresh_walk_target=false;
},
Instruction::Other(OtherInstruction::Other(OtherOtherInstruction::Idle))=>{
//literally idle!
b_refresh_walk_target=false;
},
}
if b_refresh_walk_target{
state.apply_input_and_body(data);
state.cull_velocity(data,state.body.velocity);
//also check if accelerating away from surface
}
}
#[cfg(test)]
mod test{
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));

@ -1,242 +1,67 @@
use strafesnet_common::mouse::MouseState;
use strafesnet_common::physics::Instruction as PhysicsInputInstruction;
use strafesnet_common::integer::Time;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::timer::{Scaled,Timer,TimerState};
use mouse_interpolator::MouseInterpolator;
use crate::graphics_worker::Instruction as GraphicsInstruction;
use crate::session::{SessionInputInstruction,Instruction as SessionInstruction,Session,Simulation};
use strafesnet_common::instruction::{TimedInstruction,InstructionConsumer};
use strafesnet_common::physics::Time as PhysicsTime;
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::timer::Timer;
#[derive(Debug)]
pub enum InputInstruction{
MoveMouse(glam::IVec2),
MoveRight(bool),
MoveUp(bool),
MoveBack(bool),
MoveLeft(bool),
MoveDown(bool),
MoveForward(bool),
Jump(bool),
Zoom(bool),
ResetAndRestart,
ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId,strafesnet_common::gameplay_modes::StageId),
PracticeFly,
}
pub enum Instruction{
Input(InputInstruction),
Input(SessionInputInstruction),
SetPaused(bool),
Render,
Resize(winit::dpi::PhysicalSize<u32>),
ChangeMap(strafesnet_common::map::CompleteMap),
//SetPaused is not an InputInstruction: the physics doesn't know that it's paused.
SetPaused(bool),
//Graphics(crate::graphics_worker::Instruction),
}
mod mouse_interpolator{
use super::*;
//TODO: move this or tab
pub struct MouseInterpolator{
//"PlayerController"
user_settings:crate::settings::UserSettings,
//"MouseInterpolator"
timeline:std::collections::VecDeque<TimedInstruction<PhysicsInputInstruction>>,
last_mouse_time:Time,//this value is pre-transformed to simulation time
mouse_blocking:bool,
//"Simulation"
timer:Timer<Scaled>,
physics:crate::physics::PhysicsContext,
}
impl MouseInterpolator{
pub fn new(
physics:crate::physics::PhysicsContext,
user_settings:crate::settings::UserSettings,
)->MouseInterpolator{
MouseInterpolator{
mouse_blocking:true,
last_mouse_time:physics.get_next_mouse().time,
timeline:std::collections::VecDeque::new(),
timer:Timer::from_state(Scaled::identity(),false),
physics,
user_settings,
}
}
fn push_mouse_instruction(&mut self,ins:&TimedInstruction<Instruction>,m:glam::IVec2){
if self.mouse_blocking{
//tell the game state which is living in the past about its future
self.timeline.push_front(TimedInstruction{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::SetNextMouse(MouseState{time:self.timer.time(ins.time),pos:m}),
});
}else{
//mouse has just started moving again after being still for longer than 10ms.
//replace the entire mouse interpolation state to avoid an intermediate state with identical m0.t m1.t timestamps which will divide by zero
self.timeline.push_front(TimedInstruction{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::ReplaceMouse(
MouseState{time:self.last_mouse_time,pos:self.physics.get_next_mouse().pos},
MouseState{time:self.timer.time(ins.time),pos:m}
),
});
//delay physics execution until we have an interpolation target
self.mouse_blocking=true;
}
self.last_mouse_time=self.timer.time(ins.time);
}
fn push(&mut self,time:Time,phys_input:PhysicsInputInstruction){
//This is always a non-mouse event
self.timeline.push_back(TimedInstruction{
time:self.timer.time(time),
instruction:phys_input,
});
}
/// returns should_empty_queue
/// may or may not mutate internal state XD!
fn map_instruction(&mut self,ins:&TimedInstruction<Instruction>)->bool{
let mut update_mouse_blocking=true;
match &ins.instruction{
Instruction::Input(input_instruction)=>match input_instruction{
&InputInstruction::MoveMouse(m)=>{
if !self.timer.is_paused(){
self.push_mouse_instruction(ins,m);
}
update_mouse_blocking=false;
},
&InputInstruction::MoveForward(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveForward(s)),
&InputInstruction::MoveLeft(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveLeft(s)),
&InputInstruction::MoveBack(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveBack(s)),
&InputInstruction::MoveRight(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveRight(s)),
&InputInstruction::MoveUp(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveUp(s)),
&InputInstruction::MoveDown(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveDown(s)),
&InputInstruction::Jump(s)=>self.push(ins.time,PhysicsInputInstruction::SetJump(s)),
&InputInstruction::Zoom(s)=>self.push(ins.time,PhysicsInputInstruction::SetZoom(s)),
&InputInstruction::ResetAndSpawn(mode_id,stage_id)=>{
self.push(ins.time,PhysicsInputInstruction::Reset);
self.push(ins.time,PhysicsInputInstruction::SetSensitivity(self.user_settings.calculate_sensitivity()));
self.push(ins.time,PhysicsInputInstruction::Spawn(mode_id,stage_id));
},
InputInstruction::ResetAndRestart=>{
self.push(ins.time,PhysicsInputInstruction::Reset);
self.push(ins.time,PhysicsInputInstruction::SetSensitivity(self.user_settings.calculate_sensitivity()));
self.push(ins.time,PhysicsInputInstruction::Restart);
},
InputInstruction::PracticeFly=>self.push(ins.time,PhysicsInputInstruction::PracticeFly),
},
//do these really need to idle the physics?
//sending None dumps the instruction queue
Instruction::ChangeMap(_)=>self.push(ins.time,PhysicsInputInstruction::Idle),
Instruction::Resize(_)=>self.push(ins.time,PhysicsInputInstruction::Idle),
Instruction::Render=>self.push(ins.time,PhysicsInputInstruction::Idle),
&Instruction::SetPaused(paused)=>{
if let Err(e)=self.timer.set_paused(ins.time,paused){
println!("Cannot pause: {e}");
}
self.push(ins.time,PhysicsInputInstruction::Idle);
},
}
if update_mouse_blocking{
//this returns the bool for us
self.update_mouse_blocking(ins.time)
}else{
//do flush that queue
true
}
}
/// must check if self.mouse_blocking==true before calling!
fn unblock_mouse(&mut self,time:Time){
//push an event to extrapolate no movement from
self.timeline.push_front(TimedInstruction{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::SetNextMouse(MouseState{time:self.timer.time(time),pos:self.physics.get_next_mouse().pos}),
});
self.last_mouse_time=self.timer.time(time);
//stop blocking. the mouse is not moving so the physics does not need to live in the past and wait for interpolation targets.
self.mouse_blocking=false;
}
fn update_mouse_blocking(&mut self,time:Time)->bool{
if self.mouse_blocking{
//assume the mouse has stopped moving after 10ms.
//shitty mice are 125Hz which is 8ms so this should cover that.
//setting this to 100us still doesn't print even though it's 10x lower than the polling rate,
//so mouse events are probably not handled separately from drawing and fire right before it :(
if Time::from_millis(10)<self.timer.time(time)-self.physics.get_next_mouse().time{
self.unblock_mouse(time);
true
}else{
false
}
}else{
//keep this up to date so that it can be used as a known-timestamp
//that the mouse was not moving when the mouse starts moving again
self.last_mouse_time=self.timer.time(time);
true
}
}
fn empty_queue(&mut self){
while let Some(instruction)=self.timeline.pop_front(){
self.physics.run_input_instruction(instruction);
}
}
pub fn handle_instruction(&mut self,ins:&TimedInstruction<Instruction>){
let should_empty_queue=self.map_instruction(ins);
if should_empty_queue{
self.empty_queue();
}
}
pub fn get_frame_state(&self,time:Time)->crate::graphics::FrameState{
crate::graphics::FrameState{
body:self.physics.camera_body(),
camera:self.physics.camera(),
time:self.timer.time(time),
}
}
pub fn change_map(&mut self,time:Time,map:&strafesnet_common::map::CompleteMap){
//dump any pending interpolation state
if self.mouse_blocking{
self.unblock_mouse(time);
}
self.empty_queue();
//doing it like this to avoid doing PhysicsInstruction::ChangeMap(Rc<CompleteMap>)
self.physics.generate_models(&map);
//use the standard input interface so the instructions are written out to bots
self.handle_instruction(&TimedInstruction{
time:self.timer.time(time),
instruction:Instruction::Input(InputInstruction::ResetAndSpawn(
strafesnet_common::gameplay_modes::ModeId::MAIN,
strafesnet_common::gameplay_modes::StageId::FIRST,
)),
});
}
pub const fn user_settings(&self)->&crate::settings::UserSettings{
&self.user_settings
}
}
}
const SESSION_INSTRUCTION_IDLE:SessionInstruction=SessionInstruction::Input(SessionInputInstruction::Other(strafesnet_common::physics::OtherOtherInstruction::Idle));
pub fn new<'a>(
mut graphics_worker:crate::compat_worker::INWorker<'a,crate::graphics_worker::Instruction>,
user_settings:crate::settings::UserSettings,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction>>{
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
let physics=crate::physics::PhysicsContext::default();
let mut interpolator=MouseInterpolator::new(
physics,
user_settings
let timer=Timer::unpaused(SessionTime::ZERO,PhysicsTime::ZERO);
let simulation=Simulation::new(timer,physics);
let mut session=Session::new(
user_settings,
simulation,
);
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction>|{
interpolator.handle_instruction(&ins);
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction,SessionTimeInner>|{
// excruciating pain
macro_rules! run_session_instruction{
($time:expr,$instruction:expr)=>{
session.process_instruction(TimedInstruction{
time:$time,
instruction:$instruction,
});
};
}
macro_rules! run_graphics_worker_instruction{
($instruction:expr)=>{
graphics_worker.send($instruction).unwrap();
};
}
match ins.instruction{
Instruction::Input(unbuffered_instruction)=>{
run_session_instruction!(ins.time,SessionInstruction::Input(unbuffered_instruction));
},
Instruction::SetPaused(paused)=>{
run_session_instruction!(ins.time,SessionInstruction::SetPaused(paused));
},
Instruction::Render=>{
let frame_state=interpolator.get_frame_state(ins.time);
graphics_worker.send(crate::graphics_worker::Instruction::Render(frame_state)).unwrap();
run_session_instruction!(ins.time,SESSION_INSTRUCTION_IDLE);
let frame_state=session.get_frame_state(ins.time);
run_graphics_worker_instruction!(GraphicsInstruction::Render(frame_state));
},
Instruction::Resize(size)=>{
graphics_worker.send(crate::graphics_worker::Instruction::Resize(size,interpolator.user_settings().clone())).unwrap();
Instruction::Resize(physical_size)=>{
run_session_instruction!(ins.time,SESSION_INSTRUCTION_IDLE);
let user_settings=session.user_settings().clone();
run_graphics_worker_instruction!(GraphicsInstruction::Resize(physical_size,user_settings));
},
Instruction::ChangeMap(map)=>{
interpolator.change_map(ins.time,&map);
graphics_worker.send(crate::graphics_worker::Instruction::ChangeMap(map)).unwrap();
Instruction::ChangeMap(complete_map)=>{
run_session_instruction!(ins.time,SessionInstruction::ChangeMap(&complete_map));
run_graphics_worker_instruction!(GraphicsInstruction::ChangeMap(complete_map));
},
Instruction::Input(_)=>(),
Instruction::SetPaused(_)=>(),
}
})
}

@ -0,0 +1,349 @@
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
// This algorithm is based on Lua code
// written by Trey Reynolds in 2021
// EPSILON=1/2^10
// A stack-allocated variable-size list that holds up to 4 elements
// Direct references are used instead of indices i0, i1, i2, i3
type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
// hack to allow comparing ratios to zero
const RATIO_ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
struct Ray{
origin:Planar64Vec3,
direction:Planar64Vec3,
}
impl Ray{
fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
{
self.origin+self.direction.map(|elem|(t*elem).divide().fix())
}
}
/// Information about a contact restriction
pub struct Contact{
pub position:Planar64Vec3,
pub velocity:Planar64Vec3,
pub normal:Planar64Vec3,
}
impl Contact{
fn relative_to(&self,point:Planar64Vec3)->Self{
Self{
position:self.position-point,
velocity:self.velocity,
normal:self.normal,
}
}
fn relative_dot(&self,direction:Planar64Vec3)->Fixed<2,64>{
(direction-self.velocity).dot(self.normal)
}
/// Calculate the time of intersection. (previously get_touch_time)
fn solve(&self,ray:&Ray)->Ratio<Fixed<2,64>,Fixed<2,64>>{
(self.position-ray.origin).dot(self.normal)/(ray.direction-self.velocity).dot(self.normal)
}
}
//note that this is horrible with fixed point arithmetic
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
let det=c0.normal.dot(c0.velocity);
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
Some(c0.normal*d0/det)
}
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
let u0_u1=c0.velocity.cross(c1.velocity);
let n0_n1=c0.normal.cross(c1.normal);
let det=u0_u1.dot(n0_n1);
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
let d1=c1.normal.dot(c1.position);
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
}
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
let d1=c1.normal.dot(c1.position);
let d2=c2.normal.dot(c2.position);
Some((c1.normal.cross(c2.normal)*d0+c2.normal.cross(c0.normal)*d1+c0.normal.cross(c1.normal)*d2)/det)
}
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fixed<2,64>>;1]>{
let det=u0.dot(u0);
if det==Fixed::ZERO{
return None;
}
let s0=u0.dot(point)/det;
Some([s0])
}
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<Fixed<4,128>,Fixed<4,128>>;2]>{
let u0_u1=u0.cross(u1);
let det=u0_u1.dot(u0_u1);
if det==Fixed::ZERO{
return None;
}
let s0=u0_u1.dot(point.cross(u1))/det;
let s1=u0_u1.dot(u0.cross(point))/det;
Some([s0,s1])
}
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<Fixed<3,96>,Fixed<3,96>>;3]>{
let det=u0.cross(u1).dot(u2);
if det==Fixed::ZERO{
return None;
}
let s0=point.cross(u1).dot(u2)/det;
let s1=u0.cross(point).dot(u2)/det;
let s2=u0.cross(u1).dot(point)/det;
Some([s0,s1,s2])
}
fn is_space_enclosed_2(
a:Planar64Vec3,
b:Planar64Vec3,
)->bool{
a.cross(b)==Vector3::new([Fixed::ZERO;3])
&&a.dot(b).is_negative()
}
fn is_space_enclosed_3(
a:Planar64Vec3,
b:Planar64Vec3,
c:Planar64Vec3
)->bool{
a.cross(b).dot(c)==Fixed::ZERO
&&{
let det_abac=a.cross(b).dot(a.cross(c));
let det_abbc=a.cross(b).dot(b.cross(c));
let det_acbc=a.cross(c).dot(b.cross(c));
return!( det_abac*det_abbc).is_positive()
&&!( det_abbc*det_acbc).is_positive()
&&!(-det_acbc*det_abac).is_positive()
||is_space_enclosed_2(a,b)
||is_space_enclosed_2(a,c)
||is_space_enclosed_2(b,c)
}
}
fn is_space_enclosed_4(
a:Planar64Vec3,
b:Planar64Vec3,
c:Planar64Vec3,
d:Planar64Vec3,
)->bool{
let det_abc=a.cross(b).dot(c);
let det_abd=a.cross(b).dot(d);
let det_acd=a.cross(c).dot(d);
let det_bcd=b.cross(c).dot(d);
return( det_abc*det_abd).is_negative()
&&(-det_abc*det_acd).is_negative()
&&( det_abd*det_acd).is_negative()
&&( det_abc*det_bcd).is_negative()
&&(-det_abd*det_bcd).is_negative()
&&( det_acd*det_bcd).is_negative()
||is_space_enclosed_3(a,b,c)
||is_space_enclosed_3(a,b,d)
||is_space_enclosed_3(a,c,d)
||is_space_enclosed_3(b,c,d)
}
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
Ray{origin:point,direction:vec3::ZERO}
}
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
let direction=solve1(c0)?.divide().fix_1();
let [s0]=decompose1(direction,c0.velocity)?;
if s0.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve1(
&c0.relative_to(point),
)?.divide().fix_1();
Some(Ray{origin,direction})
}
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let direction=solve2(c0,c1)?.divide().fix_1();
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve2(
&c0.relative_to(point),
&c1.relative_to(point),
)?.divide().fix_1();
Some(Ray{origin,direction})
}
fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ray>{
let direction=solve3(c0,c1,c2)?.divide().fix_1();
let [s0,s1,s2]=decompose3(direction,c0.velocity,c1.velocity,c2.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO)||s2.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve3(
&c0.relative_to(point),
&c1.relative_to(point),
&c2.relative_to(point),
)?.divide().fix_1();
Some(Ray{origin,direction})
}
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
(get_push_ray_0(point),Conts::new_const())
}
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
get_push_ray_1(point,c0)
.map(|ray|(ray,Conts::from_iter([c0])))
}
fn get_best_push_ray_and_conts_2<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact)->Option<(Ray,Conts<'a>)>{
if is_space_enclosed_2(c0.normal,c1.normal){
return None;
}
if let Some(ray)=get_push_ray_2(point,c0,c1){
return Some((ray,Conts::from_iter([c0,c1])));
}
if let Some(ray)=get_push_ray_1(point,c0){
if !c1.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0])));
}
}
return None;
}
fn get_best_push_ray_and_conts_3<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact)->Option<(Ray,Conts<'a>)>{
if is_space_enclosed_3(c0.normal,c1.normal,c2.normal){
return None;
}
if let Some(ray)=get_push_ray_3(point,c0,c1,c2){
return Some((ray,Conts::from_iter([c0,c1,c2])));
}
if let Some(ray)=get_push_ray_2(point,c0,c1){
if !c2.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0,c1])));
}
}
if let Some(ray)=get_push_ray_2(point,c0,c2){
if !c1.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0,c2])));
}
}
if let Some(ray)=get_push_ray_1(point,c0){
if !c1.relative_dot(ray.direction).is_negative()
&&!c2.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0])));
}
}
return None;
}
fn get_best_push_ray_and_conts_4<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact,c3:&'a Contact)->Option<(Ray,Conts<'a>)>{
if is_space_enclosed_4(c0.normal,c1.normal,c2.normal,c3.normal){
return None;
}
let (ray012,conts012)=get_best_push_ray_and_conts_3(point,c0,c1,c2)?;
let (ray013,conts013)=get_best_push_ray_and_conts_3(point,c0,c1,c3)?;
let (ray023,conts023)=get_best_push_ray_and_conts_3(point,c0,c2,c3)?;
let err012=c3.relative_dot(ray012.direction);
let err013=c2.relative_dot(ray013.direction);
let err023=c1.relative_dot(ray023.direction);
let best_err=err012.max(err013).max(err023);
if best_err==err012{
return Some((ray012,conts012))
}else if best_err==err013{
return Some((ray013,conts013))
}else if best_err==err023{
return Some((ray023,conts023))
}
unreachable!()
}
fn get_best_push_ray_and_conts<'a>(
point:Planar64Vec3,
conts:&[&'a Contact],
)->Option<(Ray,Conts<'a>)>{
match conts{
&[c0,c1,c2,c3]=>get_best_push_ray_and_conts_4(point,c0,c1,c2,c3),
&[c0,c1,c2]=>get_best_push_ray_and_conts_3(point,c0,c1,c2),
&[c0,c1]=>get_best_push_ray_and_conts_2(point,c0,c1),
&[c0]=>get_best_push_ray_and_conts_1(point,c0),
&[]=>Some(get_best_push_ray_and_conts_0(point)),
_=>unreachable!(),
}
}
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))
&&contact.relative_dot(ray.direction).is_negative()
)
.map(|contact|(contact.solve(ray),contact))
.min_by_key(|&(t,_)|t)
}
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){
Some((t,cont))=>(t,cont),
None=>return ray.origin,
};
if RATIO_ZERO.le_ratio(next_t){
return ray.origin;
}
//push_front
if conts.len()==conts.capacity(){
//this is a dead case, new_conts never has more than 3 elements
conts.rotate_right(1);
conts[0]=next_cont;
}else{
conts.push(next_cont);
conts.rotate_right(1);
}
let meet_point=ray.extrapolate(next_t);
match get_best_push_ray_and_conts(meet_point,conts.as_slice()){
Some((new_ray,new_conts))=>(ray,conts)=(new_ray,new_conts),
None=>return meet_point,
}
}
}
#[cfg(test)]
mod tests{
use super::*;
#[test]
fn test_push_solve(){
let contacts=vec![
Contact{
position:vec3::ZERO,
velocity:vec3::Y,
normal:vec3::Y,
}
];
assert_eq!(
vec3::ZERO,
push_solve(&contacts,vec3::NEG_Y)
);
}
}

@ -0,0 +1,193 @@
use strafesnet_common::gameplay_modes::{ModeId,StageId};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
// session represents the non-hardware state of the client.
// Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
use strafesnet_common::physics::{
ModeInstruction,OtherInstruction,OtherOtherInstruction,
Instruction as PhysicsInputInstruction,
TimeInner as PhysicsTimeInner,
Time as PhysicsTime
};
use strafesnet_common::timer::{Scaled,Timer};
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
use crate::settings::UserSettings;
pub enum Instruction<'a>{
Input(SessionInputInstruction),
SetPaused(bool),
ChangeMap(&'a strafesnet_common::map::CompleteMap),
//Graphics(crate::graphics_worker::Instruction),
}
pub enum SessionInputInstruction{
Mouse(glam::IVec2),
SetControl(strafesnet_common::physics::SetControlInstruction),
Mode(ImplicitModeInstruction),
Other(strafesnet_common::physics::OtherOtherInstruction),
}
/// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
#[derive(Clone,Debug)]
pub enum ImplicitModeInstruction{
ResetAndRestart,
ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId,strafesnet_common::gameplay_modes::StageId),
}
pub struct FrameState{
pub body:crate::physics::Body,
pub camera:crate::physics::PhysicsCamera,
pub time:PhysicsTime,
}
pub struct Simulation{
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsContext,
}
impl Simulation{
pub const fn new(
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsContext,
)->Self{
Self{
timer,
physics,
}
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
FrameState{
body:self.physics.camera_body(),
camera:self.physics.camera(),
time:self.timer.time(time),
}
}
}
pub struct Replay{
last_instruction_id:usize,
instructions:Vec<PhysicsInputInstruction>,
simulation:Simulation,
}
impl Replay{
pub const fn new(
instructions:Vec<PhysicsInputInstruction>,
simulation:Simulation,
)->Self{
Self{
last_instruction_id:0,
instructions,
simulation,
}
}
}
pub struct Session{
user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
//gui:GuiState
simulation:Simulation,
replays:Vec<Replay>,
}
impl Session{
pub fn new(
user_settings:UserSettings,
simulation:Simulation,
)->Self{
Self{
user_settings,
mouse_interpolator:MouseInterpolator::new(),
simulation,
replays:Vec::new(),
}
}
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.simulation.physics.generate_models(map);
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
self.simulation.get_frame_state(time)
}
pub fn user_settings(&self)->&UserSettings{
&self.user_settings
}
}
// mouseinterpolator consumes RawInputInstruction
// mouseinterpolator emits PhysicsInputInstruction
// mouseinterpolator consumes DoStep to move on to the next emitted instruction
// Session comsumes SessionInstruction -> forwards RawInputInstruction to mouseinterpolator
// Session consumes DoStep -> forwards DoStep to mouseinterpolator
// Session emits DoStep
impl InstructionConsumer<Instruction<'_>> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
macro_rules! run_mouse_interpolator_instruction{
($instruction:expr)=>{
self.mouse_interpolator.process_instruction(TimedInstruction{
time:ins.time,
instruction:TimedInstruction{
time:self.simulation.timer.time(ins.time),
instruction:$instruction,
},
});
};
}
match ins.instruction{
// send it down to MouseInterpolator with two timestamps, SessionTime and PhysicsTime
Instruction::Input(SessionInputInstruction::Mouse(pos))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::MoveMouse(pos));
},
Instruction::Input(SessionInputInstruction::SetControl(set_control_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::SetControl(set_control_instruction)));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndRestart))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Mode(ModeInstruction::Reset)));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Other(OtherOtherInstruction::SetSensitivity(self.user_settings().calculate_sensitivity()))));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Mode(ModeInstruction::Restart)));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Mode(ModeInstruction::Reset)));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Other(OtherOtherInstruction::SetSensitivity(self.user_settings().calculate_sensitivity()))));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id))));
},
Instruction::Input(SessionInputInstruction::Other(other_other_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Other(other_other_instruction)));
},
Instruction::SetPaused(paused)=>{
// don't flush the buffered instructions in the mouse interpolator
// until the mouse is confirmed to be not moving at a later time
// what if they pause for 5ms lmao
_=self.simulation.timer.set_paused(ins.time,paused);
}
Instruction::ChangeMap(complete_map)=>{
self.change_map(complete_map);
// ResetAndSpawn
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Mode(ModeInstruction::Reset)));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Other(OtherOtherInstruction::SetSensitivity(self.user_settings().calculate_sensitivity()))));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Other(OtherInstruction::Mode(ModeInstruction::Spawn(ModeId::MAIN,StageId::FIRST))));
},
};
// run all buffered instruction produced
self.process_exhaustive(ins.time);
}
}
impl InstructionConsumer<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
// ins.time ignored???
let ins_retimed=TimedInstruction{
time:self.simulation.timer.time(ins.time),
instruction:ins.instruction,
};
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins_retimed){
self.simulation.physics.run_input_instruction(instruction);
}
}
}
impl InstructionEmitter<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
self.mouse_interpolator.next_instruction(time_limit)
}
}

@ -1,6 +1,7 @@
use crate::window::WindowInstruction;
use crate::window::Instruction;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::integer;
use strafesnet_common::session::TimeInner as SessionTimeInner;
fn optional_features()->wgpu::Features{
wgpu::Features::TEXTURE_COMPRESSION_ASTC
@ -32,16 +33,6 @@ fn create_window(title:&str,event_loop:&winit::event_loop::EventLoop<()>)->Resul
use winit::platform::windows::WindowBuilderExtWindows;
builder=builder.with_no_redirection_bitmap(true);
}
#[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{
@ -71,18 +62,36 @@ struct SetupContextPartial2<'a>{
surface:wgpu::Surface<'a>,
}
impl<'a> SetupContextPartial2<'a>{
async fn pick_adapter(self)->SetupContextPartial3<'a>{
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();
let chosen_adapter=self.instance.request_adapter(&wgpu::RequestAdapterOptions{
power_preference:wgpu::PowerPreference::HighPerformance,
force_fallback_adapter:false,
compatible_surface:Some(&self.surface),
}).await;
//no helper function smh gotta write it myself
let adapters=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;
@ -121,7 +130,7 @@ struct SetupContextPartial3<'a>{
adapter:wgpu::Adapter,
}
impl<'a> SetupContextPartial3<'a>{
async fn request_device(self)->SetupContextPartial4<'a>{
fn request_device(self)->SetupContextPartial4<'a>{
let optional_features=optional_features();
let required_features=required_features();
@ -129,7 +138,7 @@ impl<'a> SetupContextPartial3<'a>{
let needed_limits=required_limits().using_resolution(self.adapter.limits());
let trace_dir=std::env::var("WGPU_TRACE");
let (device, queue)=self.adapter
let (device, queue)=pollster::block_on(self.adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
@ -138,7 +147,7 @@ impl<'a> SetupContextPartial3<'a>{
memory_hints:wgpu::MemoryHints::Performance,
},
trace_dir.ok().as_ref().map(std::path::Path::new),
).await
))
.expect("Unable to find a suitable GPU adapter!");
SetupContextPartial4{
@ -184,20 +193,20 @@ pub struct SetupContext<'a>{
pub config:wgpu::SurfaceConfiguration,
}
pub async fn setup_and_start(title:String){
pub fn setup_and_start(title:&str){
let event_loop=winit::event_loop::EventLoop::new().unwrap();
println!("Initializing the surface...");
let partial_1=create_instance();
let window=create_window(title.as_str(),&event_loop).unwrap();
let window=create_window(title,&event_loop).unwrap();
let partial_2=partial_1.create_surface(&window).unwrap();
let partial_3=partial_2.pick_adapter().await;
let partial_3=partial_2.pick_adapter();
let partial_4=partial_3.request_device().await;
let partial_4=partial_3.request_device();
let size=window.inner_size();
@ -215,22 +224,22 @@ pub async fn setup_and_start(title:String){
let path=std::path::PathBuf::from(arg);
window_thread.send(TimedInstruction{
time:integer::Time::ZERO,
instruction:WindowInstruction::WindowEvent(winit::event::WindowEvent::DroppedFile(path)),
instruction:Instruction::WindowEvent(winit::event::WindowEvent::DroppedFile(path)),
}).unwrap();
};
println!("Entering event loop...");
let root_time=chrono::Utc::now();
let root_time=std::time::Instant::now();
run_event_loop(event_loop,window_thread,root_time).unwrap();
}
fn run_event_loop(
event_loop:winit::event_loop::EventLoop<()>,
mut window_thread:crate::compat_worker::QNWorker<TimedInstruction<WindowInstruction>>,
root_time:chrono::DateTime<chrono::Utc>,
mut window_thread:crate::compat_worker::QNWorker<TimedInstruction<Instruction,SessionTimeInner>>,
root_time:std::time::Instant
)->Result<(),winit::error::EventLoopError>{
event_loop.run(move |event,elwt|{
let time=integer::Time::from_nanos((chrono::Utc::now()-root_time).num_nanoseconds().unwrap());
let time=integer::Time::from_nanos(root_time.elapsed().as_nanos() as i64);
// *control_flow=if cfg!(feature="metal-auto-capture"){
// winit::event_loop::ControlFlow::Exit
// }else{
@ -238,7 +247,7 @@ fn run_event_loop(
// };
match event{
winit::event::Event::AboutToWait=>{
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::RequestRedraw}).unwrap();
window_thread.send(TimedInstruction{time,instruction:Instruction::RequestRedraw}).unwrap();
}
winit::event::Event::WindowEvent {
event:
@ -250,7 +259,7 @@ fn run_event_loop(
winit::event::WindowEvent::Resized(size),//ignoring scale factor changed for now because mutex bruh
window_id:_,
} => {
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::Resize(size)}).unwrap();
window_thread.send(TimedInstruction{time,instruction:Instruction::Resize(size)}).unwrap();
}
winit::event::Event::WindowEvent{event,..}=>match event{
winit::event::WindowEvent::KeyboardInput{
@ -266,17 +275,17 @@ fn run_event_loop(
elwt.exit();
}
winit::event::WindowEvent::RedrawRequested=>{
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::Render}).unwrap();
window_thread.send(TimedInstruction{time,instruction:Instruction::Render}).unwrap();
}
_=>{
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::WindowEvent(event)}).unwrap();
window_thread.send(TimedInstruction{time,instruction:Instruction::WindowEvent(event)}).unwrap();
}
},
winit::event::Event::DeviceEvent{
event,
..
} => {
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::DeviceEvent(event)}).unwrap();
window_thread.send(TimedInstruction{time,instruction:Instruction::DeviceEvent(event)}).unwrap();
},
_=>{}
}

@ -1,8 +1,10 @@
use crate::physics_worker::InputInstruction;
use strafesnet_common::integer;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::physics::{OtherInstruction,OtherOtherInstruction,SetControlInstruction};
use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
use crate::session::SessionInputInstruction;
pub enum WindowInstruction{
pub enum Instruction{
Resize(winit::dpi::PhysicalSize<u32>),
WindowEvent(winit::event::WindowEvent),
DeviceEvent(winit::event::DeviceEvent),
@ -13,21 +15,21 @@ pub enum WindowInstruction{
//holds thread handles to dispatch to
struct WindowContext<'a>{
manual_mouse_lock:bool,
mouse:strafesnet_common::mouse::MouseState,//std::sync::Arc<std::sync::Mutex<>>
mouse_pos:glam::DVec2,
screen_size:glam::UVec2,
window:&'a winit::window::Window,
physics_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<crate::physics_worker::Instruction>>,
physics_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<PhysicsWorkerInstruction,SessionTimeInner>>,
}
impl WindowContext<'_>{
fn get_middle_of_screen(&self)->winit::dpi::PhysicalPosition<u32>{
winit::dpi::PhysicalPosition::new(self.screen_size.x/2,self.screen_size.y/2)
}
fn window_event(&mut self,time:integer::Time,event:winit::event::WindowEvent){
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()){
Ok(map)=>self.physics_thread.send(TimedInstruction{time,instruction:crate::physics_worker::Instruction::ChangeMap(map)}).unwrap(),
Ok(map)=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
Err(e)=>println!("Failed to load map: {e}"),
}
},
@ -35,7 +37,7 @@ impl WindowContext<'_>{
//pause unpause
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::SetPaused(!state),
instruction:PhysicsWorkerInstruction::SetPaused(!state),
}).unwrap();
//recalculate pressed keys on focus
},
@ -90,29 +92,29 @@ impl WindowContext<'_>{
},
(keycode,state)=>{
let s=state.is_pressed();
if let Some(input_instruction)=match keycode{
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space)=>Some(InputInstruction::Jump(s)),
if let Some(session_input_instruction)=match keycode{
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space)=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetJump(s))),
winit::keyboard::Key::Character(key)=>match key.as_str(){
"W"|"w"=>Some(InputInstruction::MoveForward(s)),
"A"|"a"=>Some(InputInstruction::MoveLeft(s)),
"S"|"s"=>Some(InputInstruction::MoveBack(s)),
"D"|"d"=>Some(InputInstruction::MoveRight(s)),
"E"|"e"=>Some(InputInstruction::MoveUp(s)),
"Q"|"q"=>Some(InputInstruction::MoveDown(s)),
"Z"|"z"=>Some(InputInstruction::Zoom(s)),
"R"|"r"=>if s{
"W"|"w"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetMoveForward(s))),
"A"|"a"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetMoveLeft(s))),
"S"|"s"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetMoveBack(s))),
"D"|"d"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetMoveRight(s))),
"E"|"e"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetMoveUp(s))),
"Q"|"q"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetMoveDown(s))),
"Z"|"z"=>Some(SessionInputInstruction::SetControl(SetControlInstruction::SetZoom(s))),
"R"|"r"=>s.then(||{
//mouse needs to be reset since the position is absolute
self.mouse=strafesnet_common::mouse::MouseState::default();
Some(InputInstruction::ResetAndRestart)
}else{None},
"F"|"f"=>if s{Some(InputInstruction::PracticeFly)}else{None},
self.mouse_pos=glam::DVec2::ZERO;
SessionInputInstruction::Mode(crate::session::ImplicitModeInstruction::ResetAndRestart)
}),
"F"|"f"=>s.then_some(SessionInputInstruction::Other(OtherOtherInstruction::PracticeFly)),
_=>None,
},
_=>None,
}{
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Input(input_instruction),
instruction:PhysicsWorkerInstruction::Input(session_input_instruction),
}).unwrap();
}
},
@ -122,10 +124,10 @@ impl WindowContext<'_>{
}
}
fn device_event(&mut self,time:integer::Time,event: winit::event::DeviceEvent){
fn device_event(&mut self,time:SessionTime,event: winit::event::DeviceEvent){
match event{
winit::event::DeviceEvent::MouseMotion{
delta,//these (f64,f64) are integers on my machine
delta,
}=>{
if self.manual_mouse_lock{
match self.window.set_cursor_position(self.get_middle_of_screen()){
@ -133,14 +135,10 @@ impl WindowContext<'_>{
Err(e)=>println!("Could not set cursor position: {:?}",e),
}
}
//do not step the physics because the mouse polling rate is higher than the physics can run.
//essentially the previous input will be overwritten until a true step runs
//which is fine because they run all the time.
let delta=glam::ivec2(delta.0 as i32,delta.1 as i32);
self.mouse.pos+=delta;
self.mouse_pos+=glam::dvec2(delta.0,delta.1);
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Input(InputInstruction::MoveMouse(self.mouse.pos)),
instruction:PhysicsWorkerInstruction::Input(SessionInputInstruction::Mouse(self.mouse_pos.as_ivec2())),
}).unwrap();
},
winit::event::DeviceEvent::MouseWheel {
@ -150,7 +148,7 @@ impl WindowContext<'_>{
if false{//self.physics.style.use_scroll{
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Input(InputInstruction::Jump(true)),//activates the immediate jump path, but the style modifier prevents controls&CONTROL_JUMP bit from being set to auto jump
instruction:PhysicsWorkerInstruction::Input(SessionInputInstruction::SetControl(SetControlInstruction::SetJump(true))),//activates the immediate jump path, but the style modifier prevents controls&CONTROL_JUMP bit from being set to auto jump
}).unwrap();
}
},
@ -161,7 +159,7 @@ impl WindowContext<'_>{
pub fn worker<'a>(
window:&'a winit::window::Window,
setup_context:crate::setup::SetupContext<'a>,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<WindowInstruction>>{
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
// WindowContextSetup::new
let user_settings=crate::settings::read_user_settings();
@ -173,7 +171,7 @@ pub fn worker<'a>(
let graphics_thread=crate::graphics_worker::new(graphics,setup_context.config,setup_context.surface,setup_context.device,setup_context.queue);
let mut window_context=WindowContext{
manual_mouse_lock:false,
mouse:strafesnet_common::mouse::MouseState::default(),
mouse_pos:glam::DVec2::ZERO,
//make sure to update this!!!!!
screen_size,
window,
@ -184,30 +182,30 @@ pub fn worker<'a>(
};
//WindowContextSetup::into_worker
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<WindowInstruction>|{
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction,SessionTimeInner>|{
match ins.instruction{
WindowInstruction::RequestRedraw=>{
Instruction::RequestRedraw=>{
window_context.window.request_redraw();
}
WindowInstruction::WindowEvent(window_event)=>{
Instruction::WindowEvent(window_event)=>{
window_context.window_event(ins.time,window_event);
},
WindowInstruction::DeviceEvent(device_event)=>{
Instruction::DeviceEvent(device_event)=>{
window_context.device_event(ins.time,device_event);
},
WindowInstruction::Resize(size)=>{
Instruction::Resize(size)=>{
window_context.physics_thread.send(
TimedInstruction{
time:ins.time,
instruction:crate::physics_worker::Instruction::Resize(size)
instruction:PhysicsWorkerInstruction::Resize(size)
}
).unwrap();
}
WindowInstruction::Render=>{
Instruction::Render=>{
window_context.physics_thread.send(
TimedInstruction{
time:ins.time,
instruction:crate::physics_worker::Instruction::Render
instruction:PhysicsWorkerInstruction::Render
}
).unwrap();
}

@ -176,21 +176,21 @@ impl<'a,Task:Send+'a> INWorker<'a,Task>{
#[cfg(test)]
mod test{
use super::{thread,QRWorker};
use crate::physics;
type Body=crate::physics::Body;
use strafesnet_common::{integer,instruction};
#[test]//How to run this test with printing: cargo test --release -- --nocapture
fn test_worker() {
// Create the worker thread
let test_body=physics::Body::new(integer::vec3::ONE,integer::vec3::ONE,integer::vec3::ONE,integer::Time::ZERO);
let worker=QRWorker::new(physics::Body::ZERO,
|_|physics::Body::new(integer::vec3::ONE,integer::vec3::ONE,integer::vec3::ONE,integer::Time::ZERO)
let test_body=Body::new(integer::vec3::ONE,integer::vec3::ONE,integer::vec3::ONE,integer::Time::ZERO);
let worker=QRWorker::new(Body::ZERO,
|_|Body::new(integer::vec3::ONE,integer::vec3::ONE,integer::vec3::ONE,integer::Time::ZERO)
);
// Send tasks to the worker
for _ in 0..5 {
let task = instruction::TimedInstruction{
time:integer::Time::ZERO,
instruction:strafesnet_common::physics::Instruction::Idle,
time:strafesnet_common::physics::Time::ZERO,
instruction:strafesnet_common::physics::Instruction::IDLE,
};
worker.send(task).unwrap();
}
@ -204,7 +204,7 @@ mod test{
// Send a new task
let task = instruction::TimedInstruction{
time:integer::Time::ZERO,
instruction:strafesnet_common::physics::Instruction::Idle,
instruction:strafesnet_common::physics::Instruction::IDLE,
};
worker.send(task).unwrap();

@ -1 +0,0 @@
/dist

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

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