Compare commits

..

17 Commits

Author SHA1 Message Date
cc6450975c fix pre-pause time leak 2024-08-02 10:35:05 -07:00
500db0a5b9 warn about time travel 2024-08-02 10:28:29 -07:00
971e83e392 pre-transform timestamps 2024-08-02 10:28:19 -07:00
2d8f677501 bunch of stuff 2024-08-02 10:12:43 -07:00
3b689e58b3 wip 2024-08-02 09:43:12 -07:00
a8d54fdd7c minor tweak to loading map from arg 2024-08-02 09:20:34 -07:00
34ac541260 ignore ReachWalkTargetVelocity 2024-08-02 08:03:16 -07:00
099788b746 this is wrong, even when velocity is zero 2024-08-02 08:03:16 -07:00
305017c6c8 iso shortcut 2024-08-02 08:03:16 -07:00
e47d152af2 make a distinction between restart and spawning 2024-08-02 08:03:16 -07:00
755adeaefd refactor physics instruction processing
This is an important engine upgrade: idle events do not donate their timestamp to engine objects and pollute the timeline with unnecessary game ticks that can be represented as analytic continuations of previous game ticks.  This means that all "render" tick updates can be dropped from bot timelines.  In other words, progressing the physics simulation is invariant to differing subdivisions of an overall time advancement with no external input.
2024-08-02 08:03:16 -07:00
e04e754abb v0.10.2 run timer 2024-08-01 09:39:16 -07:00
5c1f69628d update deps 2024-08-01 09:36:11 -07:00
44031c8b83 simple run timer 2024-08-01 09:29:13 -07:00
d6470ee81b denormalize zone data on load 2024-08-01 09:29:09 -07:00
a7c7088c1f model is supposed to be guaranteed to exist 2024-07-31 12:08:57 -07:00
dd6acbfc2f use mem::replace where it is needed 2024-07-31 12:08:57 -07:00
10 changed files with 393 additions and 452 deletions

66
Cargo.lock generated

@ -28,7 +28,7 @@ dependencies = [
"getrandom",
"once_cell",
"version_check",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@ -262,9 +262,9 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.16.1"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83"
dependencies = [
"bytemuck_derive",
]
@ -294,9 +294,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9"
[[package]]
name = "calloop"
@ -802,9 +802,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.6"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [
"equivalent",
"hashbrown",
@ -1040,9 +1040,9 @@ dependencies = [
[[package]]
name = "naga"
version = "22.0.0"
version = "22.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09eeccb9b50f4f7839b214aa3e08be467159506a986c18e0702170ccf720a453"
checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad"
dependencies = [
"arrayvec",
"bit-set",
@ -1460,9 +1460,12 @@ checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f"
dependencies = [
"zerocopy 0.6.6",
]
[[package]]
name = "presser"
@ -1877,7 +1880,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strafe-client"
version = "0.10.1"
version = "0.10.2"
dependencies = [
"bytemuck",
"configparser",
@ -1909,9 +1912,9 @@ dependencies = [
[[package]]
name = "strafesnet_common"
version = "0.2.0"
version = "0.2.1"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "74580c59a09194ce39db49cd814a5c2fc2d61513c88c6b811b5b40c0da6de057"
checksum = "0ac1343bd9731706a962b66fac1efd1bd5ff56fd0a40f41c48e006fad91cf80c"
dependencies = [
"bitflags 2.6.0",
"glam",
@ -2052,9 +2055,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.7"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
@ -2396,9 +2399,9 @@ dependencies = [
[[package]]
name = "wgpu"
version = "22.0.0"
version = "22.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87e07e87a179614940ad845397e03201847453a37b43a31a3b54eee2e6e32ce"
checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433"
dependencies = [
"arrayvec",
"cfg_aliases 0.1.1",
@ -2421,9 +2424,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "22.0.0"
version = "22.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f191908a21968991463fcf3b42cb6c9648c0fb7fa301b8fc733bc21a9ed9bd"
checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a"
dependencies = [
"arrayvec",
"bit-vec",
@ -2876,13 +2879,34 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
[[package]]
name = "zerocopy"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
dependencies = [
"byteorder 1.5.0",
"zerocopy-derive 0.6.6",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy-derive"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]]

@ -1,6 +1,6 @@
[package]
name = "strafe-client"
version = "0.10.1"
version = "0.10.2"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-client"
license = "Custom"
@ -23,7 +23,7 @@ id = { version = "0.1.0", registry = "strafesnet" }
parking_lot = "0.12.1"
pollster = "0.3.0"
strafesnet_bsp_loader = { version = "0.1.3", registry = "strafesnet", optional = true }
strafesnet_common = { version = "0.2.0", registry = "strafesnet" }
strafesnet_common = { version = "0.2.1", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.3.1", features = ["legacy"], registry = "strafesnet", optional = true }
strafesnet_rbx_loader = { version = "0.3.2", registry = "strafesnet", optional = true }
strafesnet_snf = { version = "0.1.0", registry = "strafesnet", optional = true }

@ -1,6 +1,5 @@
mod file;
mod setup;
mod timer;
mod window;
mod worker;
mod physics;

@ -2,6 +2,7 @@ use std::collections::{HashMap,HashSet};
use crate::model_physics::{self,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
use strafesnet_common::bvh;
use strafesnet_common::map;
use strafesnet_common::run;
use strafesnet_common::aabb;
use strafesnet_common::model::{MeshId,ModelId};
use strafesnet_common::gameplay_attributes::{self,CollisionAttributesId};
@ -12,8 +13,10 @@ use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer
use strafesnet_common::integer::{self,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2};
use gameplay::ModeState;
//internal influence
//when the physics asks itself what happens next, this is how it's represented
#[derive(Debug)]
pub enum PhysicsInstruction {
enum PhysicsInternalInstruction{
CollisionStart(Collision),
CollisionEnd(Collision),
StrafeTick,
@ -24,12 +27,11 @@ pub enum PhysicsInstruction {
// bool,//true = Trigger; false = teleport
// bool,//true = Force
// )
//InputInstructions conditionally activate RefreshWalkTarget (by doing what SetWalkTargetVelocity used to do and then flagging it)
Input(PhysicsInputInstruction),
SetSensitivity(Ratio64Vec2),
}
//external influence
//this is how you influence the physics from outside
#[derive(Debug)]
pub enum PhysicsInputInstruction {
pub enum PhysicsInputInstruction{
ReplaceMouse(MouseState,MouseState),
SetNextMouse(MouseState),
SetMoveRight(bool),
@ -40,12 +42,21 @@ pub enum PhysicsInputInstruction {
SetMoveForward(bool),
SetJump(bool),
SetZoom(bool),
Reset,
Restart,
Spawn(gameplay_modes::ModeId,StageId),
Idle,
//Idle: there were no input events, but the simulation is safe to advance to this timestep
//for interpolation / networking / playback reasons, most playback heads will always want
//to be 1 instruction ahead to generate the next state for interpolation.
PracticeFly,
SetSensitivity(Ratio64Vec2),
}
#[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,Default,Hash)]
@ -106,6 +117,8 @@ impl InputState{
fn set_next_mouse(&mut self,next_mouse:MouseState){
//I like your functions magic language
self.mouse=std::mem::replace(&mut self.next_mouse,next_mouse);
//equivalently:
//(self.next_mouse,self.mouse)=(next_mouse,self.next_mouse.clone());
}
fn replace_mouse(&mut self,mouse:MouseState,next_mouse:MouseState){
(self.next_mouse,self.mouse)=(next_mouse,mouse);
@ -247,8 +260,8 @@ impl PhysicsModels{
&model.transform
)
}
fn model(&self,model_id:PhysicsModelId)->Option<&PhysicsModel>{
self.models.get(&model_id)
fn model(&self,model_id:PhysicsModelId)->&PhysicsModel{
&self.models[&model_id]
}
fn attr(&self,model_id:PhysicsModelId)->&PhysicsCollisionAttributes{
&self.attributes[&self.models[&model_id].attr_id]
@ -556,13 +569,13 @@ impl MoveState{
=>None,
}
}
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<PhysicsInstruction>>{
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
//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:PhysicsInstruction::ReachWalkTargetVelocity
instruction:PhysicsInternalInstruction::ReachWalkTargetVelocity
}),
TransientAcceleration::Unreachable{acceleration:_}
|TransientAcceleration::Reached
@ -572,7 +585,7 @@ impl MoveState{
TimedInstruction{
time:strafe.next_tick(time),
//only poll the physics if there is a before and after mouse event
instruction:PhysicsInstruction::StrafeTick
instruction:PhysicsInternalInstruction::StrafeTick
}
}),
MoveState::Water=>None,//TODO
@ -766,7 +779,7 @@ impl TouchingState{
}
}
}
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<PhysicsInstruction>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){
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::default(),body).body(time);
for contact in &self.contacts{
//detect face slide off
@ -775,7 +788,7 @@ impl TouchingState{
collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(_face,time)|{
TimedInstruction{
time,
instruction:PhysicsInstruction::CollisionEnd(
instruction:PhysicsInternalInstruction::CollisionEnd(
Collision::Contact(ContactCollision{convex_mesh_id:contact.convex_mesh_id,face_id:contact.face_id})
),
}
@ -788,7 +801,7 @@ impl TouchingState{
collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{
TimedInstruction{
time,
instruction:PhysicsInstruction::CollisionEnd(
instruction:PhysicsInternalInstruction::CollisionEnd(
Collision::Intersect(IntersectCollision{convex_mesh_id:intersect.convex_mesh_id})
),
}
@ -904,6 +917,10 @@ pub struct PhysicsState{
//gameplay_state
mode_state:ModeState,
move_state:MoveState,
//run is non optional: when you spawn in a run is created
//the run cannot be finished unless you start it by visiting
//a start zone. If you change mode, a new run is created.
run:run::Run,
}
//random collection of contextual data that doesn't belong in PhysicsState
pub struct PhysicsData{
@ -923,11 +940,12 @@ impl Default for PhysicsState{
time:Time::ZERO,
style:StyleModifiers::default(),
touching:TouchingState::default(),
move_state: MoveState::Air,
move_state:MoveState::Air,
camera:PhysicsCamera::default(),
input_state:InputState::default(),
world:WorldState{},
mode_state:ModeState::default(),
run:run::Run::new(),
}
}
}
@ -942,15 +960,16 @@ impl Default for PhysicsData{
}
}
impl PhysicsState {
impl PhysicsState{
fn clear(&mut self){
self.touching.clear();
}
fn advance_time(&mut self, time: Time){
self.body.advance_time(time);
self.time=time;
fn reset_to_default(&mut self){
let mut new_state=Self::default();
new_state.camera.sensitivity=self.camera.sensitivity;
*self=new_state;
}
fn next_move_instruction(&self)->Option<TimedInstruction<PhysicsInstruction>>{
fn next_move_instruction(&self)->Option<TimedInstruction<PhysicsInternalInstruction>>{
self.move_state.next_move_instruction(&self.style.strafe,self.time)
}
//lmao idk this is convenient
@ -1023,31 +1042,39 @@ 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)
}
}
impl instruction::InstructionEmitter<PhysicsInstruction> for PhysicsContext{
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<PhysicsInstruction>>{
literally_next_instruction_but_with_context(&self.state,&self.data,time_limit)
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
next_instruction_internal(&self.state,&self.data,time_limit)
}
}
impl PhysicsContext{
pub fn clear(&mut self){
self.state.clear();
}
//TODO: remove non-standard interfaces to process_instruction
pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){
self.process_instruction(TimedInstruction{
self.run_input_instruction(TimedInstruction{
time:self.state.time,
instruction:PhysicsInstruction::SetSensitivity(user_settings.calculate_sensitivity()),
instruction:PhysicsInputInstruction::SetSensitivity(user_settings.calculate_sensitivity()),
});
}
pub fn restart(&mut self){
self.run_input_instruction(TimedInstruction{
time:self.state.time,
instruction:PhysicsInputInstruction::Restart,
});
}
pub fn spawn(&mut self){
self.process_instruction(TimedInstruction{
self.run_input_instruction(TimedInstruction{
time:self.state.time,
instruction:PhysicsInstruction::Input(PhysicsInputInstruction::Reset),
instruction:PhysicsInputInstruction::Spawn(gameplay_modes::ModeId::MAIN,StageId::FIRST),
});
}
pub const fn output(&self)->PhysicsOutputState{
@ -1063,6 +1090,9 @@ impl PhysicsContext{
}
pub fn generate_models(&mut self,map:&map::CompleteMap){
self.data.modes=map.modes.clone();
for mode in &mut self.data.modes.modes{
mode.denormalize_data();
}
let mut used_attributes=Vec::new();
let mut physics_attr_id_from_model_attr_id=HashMap::<CollisionAttributesId,PhysicsAttributesId>::new();
let mut used_meshes=Vec::new();
@ -1130,16 +1160,19 @@ impl PhysicsContext{
}
//tickless gaming
fn run(&mut self,time_limit:Time){
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(instruction);
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(instruction.time);
self.run_internal_exhaustive(instruction.time);
self.process_instruction(TimedInstruction{
time:instruction.time,
instruction:PhysicsInstruction::Input(instruction.instruction),
@ -1147,7 +1180,8 @@ impl PhysicsContext{
}
}
fn literally_next_instruction_but_with_context(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<PhysicsInstruction>>{
//this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
//JUST POLLING!!! NO MUTATION
let mut collector = instruction::InstructionCollector::new(time_limit);
@ -1169,7 +1203,7 @@ impl PhysicsContext{
//temp (?) code to avoid collision loops
.map_or(None,|(face,time)|if time==state.time{None}else{Some((face,time))})
.map(|(face,time)|{
TimedInstruction{time,instruction:PhysicsInstruction::CollisionStart(match data.models.attr(convex_mesh_id.model_id){
TimedInstruction{time,instruction:PhysicsInternalInstruction::CollisionStart(match data.models.attr(convex_mesh_id.model_id){
PhysicsCollisionAttributes::Contact{contacting:_,general:_}=>Collision::Contact(ContactCollision{convex_mesh_id,face_id:face}),
PhysicsCollisionAttributes::Intersect{intersecting:_,general:_}=>Collision::Intersect(IntersectCollision{convex_mesh_id}),
})}
@ -1240,7 +1274,7 @@ fn teleport(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,sty
MoveState::Air
}
fn teleport_to_spawn(body:&mut Body,touching:&mut TouchingState,style:&StyleModifiers,hitbox_mesh:&HitboxMesh,mode:&gameplay_modes::Mode,models:&PhysicsModels,stage_id:gameplay_modes::StageId)->Option<MoveState>{
let model=models.model(mode.get_spawn_model_id(stage_id)?.into()).unwrap();
let model=models.model(mode.get_spawn_model_id(stage_id)?.into());
let point=model.transform.vertex.transform_point3(Planar64Vec3::Y)+Planar64Vec3::Y*(style.hitbox.halfsize.y()+Planar64::ONE/16);
Some(teleport(body,touching,models,style,hitbox_mesh,point))
}
@ -1296,8 +1330,8 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
}
match wormhole{
&Some(gameplay_attributes::Wormhole{destination_model})=>{
let origin_model=models.model(convex_mesh_id.model_id).unwrap();
let destination_model=models.model(destination_model.into()).unwrap();
let origin_model=models.model(convex_mesh_id.model_id);
let destination_model=models.model(destination_model.into());
//ignore the transform for now
Some(teleport(body,touching,models,style,hitbox_mesh,body.position-origin_model.transform.vertex.translation+destination_model.transform.vertex.translation))
}
@ -1305,27 +1339,18 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
}
}
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::StrafeTick=>(),
_=>println!("{}|{:?}",ins.time,ins.instruction),
}
//selectively update body
match &ins.instruction{
PhysicsInstruction::Input(PhysicsInputInstruction::Idle)=>state.time=ins.time,//idle simply updates time
PhysicsInstruction::Input(_)
|PhysicsInstruction::ReachWalkTargetVelocity
|PhysicsInstruction::CollisionStart(_)
|PhysicsInstruction::CollisionEnd(_)
|PhysicsInstruction::StrafeTick
|PhysicsInstruction::SetSensitivity(_)
=>state.advance_time(ins.time),
}
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInternalInstruction>){
let should_advance_body=match ins.instruction{
PhysicsInternalInstruction::CollisionStart(_)
|PhysicsInternalInstruction::CollisionEnd(_)
|PhysicsInternalInstruction::StrafeTick
|PhysicsInternalInstruction::ReachWalkTargetVelocity=>true,
};
if should_advance_body{
state.body.advance_time(state.time);
}
match ins.instruction{
PhysicsInstruction::CollisionStart(collision)=>{
PhysicsInternalInstruction::CollisionStart(collision)=>{
let convex_mesh_id=collision.convex_mesh_id();
match (data.models.attr(convex_mesh_id.model_id),&collision){
(PhysicsCollisionAttributes::Contact{contacting,general},&Collision::Contact(contact))=>{
@ -1409,17 +1434,32 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
//doing input_and_body to refresh the walk state if you hit a wall while accelerating
state.apply_enum_and_input_and_body(data);
},
(PhysicsCollisionAttributes::Intersect{intersecting: _,general},Collision::Intersect(intersect))=>{
(PhysicsCollisionAttributes::Intersect{intersecting:_,general},Collision::Intersect(_intersect))=>{
//I think that setting the velocity to 0 was preventing surface contacts from entering an infinite loop
state.touching.insert(collision);
if let Some(mode)=data.modes.get_mode(state.mode_state.get_mode_id()){
let zone=mode.get_zone(convex_mesh_id.model_id.into());
match zone{
Some(gameplay_modes::Zone::Start)=>{
println!("@@@@ Starting new run!");
state.run=run::Run::new();
},
Some(gameplay_modes::Zone::Finish)=>{
match state.run.finish(state.time){
Ok(())=>println!("@@@@ Finished run time={}",state.run.time(state.time)),
Err(e)=>println!("@@@@ Run Finish error:{e:?}"),
}
},
Some(gameplay_modes::Zone::Anticheat)=>state.run.flag(run::FlagReason::Anticheat),
None=>(),
}
run_teleport_behaviour(&general.wormhole,&data.models,mode,&state.style,&data.hitbox_mesh,&mut state.mode_state,&mut state.touching,&mut state.body,convex_mesh_id);
}
},
_=>panic!("invalid pair"),
}
},
PhysicsInstruction::CollisionEnd(collision)=>{
PhysicsInternalInstruction::CollisionEnd(collision)=>{
match (data.models.attr(collision.convex_mesh_id().model_id),&collision){
(PhysicsCollisionAttributes::Contact{contacting:_,general:_},&Collision::Contact(contact))=>{
state.touching.remove(&collision);//remove contact before calling contact_constrain_acceleration
@ -1436,11 +1476,23 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
},
(PhysicsCollisionAttributes::Intersect{intersecting: _,general:_},Collision::Intersect(_))=>{
state.touching.remove(&collision);
if let Some(mode)=data.modes.get_mode(state.mode_state.get_mode_id()){
let zone=mode.get_zone(collision.convex_mesh_id().model_id.into());
match zone{
Some(gameplay_modes::Zone::Start)=>{
match state.run.start(state.time){
Ok(())=>println!("@@@@ Started run"),
Err(e)=>println!("@@@@ Run Start error:{e:?}"),
}
},
_=>(),
}
}
},
_=>panic!("invalid pair"),
}
},
PhysicsInstruction::StrafeTick=>{
PhysicsInternalInstruction::StrafeTick=>{
//TODO make this less huge
if let Some(strafe_settings)=&state.style.strafe{
let controls=state.input_state.controls;
@ -1458,7 +1510,7 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
}
}
}
PhysicsInstruction::ReachWalkTargetVelocity=>{
PhysicsInternalInstruction::ReachWalkTargetVelocity=>{
match &mut state.move_state{
MoveState::Air
|MoveState::Water
@ -1482,10 +1534,46 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
}
}
},
PhysicsInstruction::SetSensitivity(sensitivity)=>state.camera.sensitivity=sensitivity,
PhysicsInstruction::Input(input_instruction)=>{
}
}
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInputInstruction>){
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::Restart
|PhysicsInputInstruction::Spawn(..)
|PhysicsInputInstruction::SetZoom(..)
|PhysicsInputInstruction::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(..)=>{
match &state.move_state{
MoveState::Fly
|MoveState::Water
|MoveState::Walk(_)
|MoveState::Ladder(_)=>true,
MoveState::Air=>false,
}
},
//the body must be updated unconditionally
PhysicsInputInstruction::PracticeFly=>true,
};
if should_advance_body{
state.body.advance_time(state.time);
}
//TODO: UNTAB
let mut b_refresh_walk_target=true;
match input_instruction{
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);
@ -1501,7 +1589,6 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
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)=>{
b_refresh_walk_target=false;
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{
@ -1510,23 +1597,32 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
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=>{
//it matters which of these runs first, but I have not thought it through yet as it doesn't matter yet
state.mode_state.clear();
state.mode_state.set_stage_id(gameplay_modes::StageId::FIRST);
let spawn_point=data.modes.get_mode(state.mode_state.get_mode_id()).and_then(|mode|
PhysicsInputInstruction::Restart=>{
//totally reset physics state
state.reset_to_default();
//spawn at start zone
let spawn_point=data.modes.get_mode(state.mode_state.get_mode_id()).map(|mode|
//TODO: spawn at the bottom of the start zone plus the hitbox size
data.models.model(mode.get_start().into()).map(|model|model.transform.vertex.translation)
//TODO: set camera andles to face the same way as the start zone
data.models.model(mode.get_start().into()).transform.vertex.translation
).unwrap_or(Planar64Vec3::ZERO);
set_position(&mut state.body,&mut state.touching,spawn_point);
set_velocity(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,Planar64Vec3::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){
teleport_to_spawn(&mut state.body,&mut state.touching,&state.style,&data.hitbox_mesh,mode,&data.models,stage_id);
}
b_refresh_walk_target=false;
},
PhysicsInputInstruction::PracticeFly=>{
match &state.move_state{
@ -1539,13 +1635,33 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
}
b_refresh_walk_target=false;
},
PhysicsInputInstruction::Idle=>{b_refresh_walk_target=false;},//literally idle!
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);
}
},
}
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! {:?}",ins);
}
state.time=ins.time;
match ins.instruction{
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}),
}
}

@ -1,6 +1,8 @@
use crate::physics::{MouseState,PhysicsInputInstruction};
use strafesnet_common::integer::Time;
use strafesnet_common::instruction::{TimedInstruction,InstructionConsumer};
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::timer::{Scaled,Timer,TimerState};
#[derive(Debug)]
pub enum InputInstruction{
MoveMouse(glam::IVec2),
@ -12,194 +14,173 @@ pub enum InputInstruction{
MoveForward(bool),
Jump(bool),
Zoom(bool),
Reset,
Restart,
Spawn(strafesnet_common::gameplay_modes::ModeId,strafesnet_common::gameplay_modes::StageId),
PracticeFly,
}
pub enum Instruction{
Passthrough(PassthroughInstruction),
Interpolate(InputInstruction),
}
pub enum PassthroughInstruction{
Input(InputInstruction),
Render,
Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings),
GenerateModels(strafesnet_common::map::CompleteMap),
ClearModels,
SetPaused(bool),
//Graphics(crate::graphics_worker::Instruction),
}
pub struct MouseInterpolator{
queue:std::collections::VecDeque<TimedInstruction<InputInstruction>>,
}
fn drain_queue(physics:&mut crate::physics::PhysicsContext,iterable:impl IntoIterator<Item=TimedInstruction<InputInstruction>>){
for ins in iterable{
let physics_input=match &ins.instruction{
InputInstruction::MoveMouse(_)=>panic!("Queue was confirmed to contain no MoveMouse events1"),
&InputInstruction::MoveForward(s)=>PhysicsInputInstruction::SetMoveForward(s),
&InputInstruction::MoveLeft(s)=>PhysicsInputInstruction::SetMoveLeft(s),
&InputInstruction::MoveBack(s)=>PhysicsInputInstruction::SetMoveBack(s),
&InputInstruction::MoveRight(s)=>PhysicsInputInstruction::SetMoveRight(s),
&InputInstruction::MoveUp(s)=>PhysicsInputInstruction::SetMoveUp(s),
&InputInstruction::MoveDown(s)=>PhysicsInputInstruction::SetMoveDown(s),
&InputInstruction::Jump(s)=>PhysicsInputInstruction::SetJump(s),
&InputInstruction::Zoom(s)=>PhysicsInputInstruction::SetZoom(s),
InputInstruction::Reset=>PhysicsInputInstruction::Reset,
InputInstruction::PracticeFly=>PhysicsInputInstruction::PracticeFly,
};
physics.run_input_instruction(TimedInstruction{
time:ins.time,
instruction:physics_input,
});
}
timeline:std::collections::VecDeque<TimedInstruction<PhysicsInputInstruction>>,
last_mouse_time:Time,//this value is pre-transformed to simulation time
mouse_blocking:bool,
timer:Timer<Scaled>,
}
impl MouseInterpolator{
fn handle_instruction(&mut self,physics:&mut crate::physics::PhysicsContext,ins:TimedInstruction<InputInstruction>){
//need to handle the case where mouse polling rate is less than 100hz
//also the whole thing is probably wrong lol
let is_inserting_mouse_instruction=matches!(ins.instruction,InputInstruction::MoveMouse(_));
self.queue.push_back(ins);
//We just pushed an element.
//The first element is guaranteed to exist.
let mut iter=self.queue.iter();
//find a mouse input
'outer:loop{
match iter.next(){
Some(ins0)=>{
let physics_input=match &ins0.instruction{
&InputInstruction::MoveMouse(mut mouse0)=>{
//mouse instruction found.
//enter a new loop with different behaviour
//we have to wait for the next mouse event
//so there is a before and after interpolation target
//write down ins0.time to appease the borrow checker
let mut t0=ins0.time;
'inner:loop{
match iter.next(){
Some(ins1)=>match &ins1.instruction{
&InputInstruction::MoveMouse(mouse1)=>{
//we found two mouse events to interpolate between
let consume_count=self.queue.len()-iter.len()-1;//don't consume the mouse1 instruction
//fire off a mouse instruction
physics.run_input_instruction(TimedInstruction{
time:t0,
instruction:PhysicsInputInstruction::SetNextMouse(
MouseState{time:ins1.time,pos:mouse1}
),
});
//update inner loop state
mouse0=mouse1;
t0=ins1.time;
//drain and handle the elements from the front
std::mem::drop(iter);
let mut hot_queue=self.queue.drain(0..consume_count);
hot_queue.next();
drain_queue(physics,hot_queue);
iter=self.queue.iter();
//keep looking for another mouse instruction in the inner loop
continue 'inner;
},
_=>if Time::from_millis(10)<ins1.time-t0{
//we have passed more than 10ms of instructions and have not seen a mouse event.
let consume_count=self.queue.len()-iter.len();
//run an event to extrapolate no movement from
let last_mouse=physics.get_next_mouse();
physics.run_input_instruction(TimedInstruction{
time:last_mouse.time,
instruction:PhysicsInputInstruction::SetNextMouse(
MouseState{time:ins1.time,pos:last_mouse.pos}
),
});
//drop the iterator so we can consume the queue up to this point
std::mem::drop(iter);
//consume queue up to the scanned point
let mut hot_queue=self.queue.drain(0..consume_count);
//the first element is always the last mouse instruction (last_mouse above)
hot_queue.next();
drain_queue(physics,hot_queue);
//make a new iterator starting from the new beginning
//and continue looping like nothing happened
iter=self.queue.iter();
continue 'outer;
},
},
None=>{
if is_inserting_mouse_instruction{
//the mouse started moving again after being still for over 10ms.
//replace the entire mouse state
physics.run_input_instruction(TimedInstruction{
time:physics.get_next_mouse().time,
instruction:PhysicsInputInstruction::ReplaceMouse(
physics.get_next_mouse().clone(),
MouseState{time:t0,pos:mouse0}
),
});
}
break 'outer;
}
}
}
},
&InputInstruction::MoveForward(s)=>PhysicsInputInstruction::SetMoveForward(s),
&InputInstruction::MoveLeft(s)=>PhysicsInputInstruction::SetMoveLeft(s),
&InputInstruction::MoveBack(s)=>PhysicsInputInstruction::SetMoveBack(s),
&InputInstruction::MoveRight(s)=>PhysicsInputInstruction::SetMoveRight(s),
&InputInstruction::MoveUp(s)=>PhysicsInputInstruction::SetMoveUp(s),
&InputInstruction::MoveDown(s)=>PhysicsInputInstruction::SetMoveDown(s),
&InputInstruction::Jump(s)=>PhysicsInputInstruction::SetJump(s),
&InputInstruction::Zoom(s)=>PhysicsInputInstruction::SetZoom(s),
InputInstruction::Reset=>PhysicsInputInstruction::Reset,
InputInstruction::PracticeFly=>PhysicsInputInstruction::PracticeFly,
};
//handle each event immediately, we are not waiting for mouse
physics.run_input_instruction(TimedInstruction{
time:ins0.time,
instruction:physics_input,
});
//drop it and pop it! consume one element and continue the loop
std::mem::drop(iter);
self.queue.pop_front();
iter=self.queue.iter();
fn push_mouse_instruction(&mut self,physics:&crate::physics::PhysicsContext,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: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);
}
/// returns the mapped physics input instruction
/// may or may not mutate internal state XD!
fn map_instruction(&mut self,physics:&crate::physics::PhysicsContext,ins:&TimedInstruction<Instruction>)->Option<PhysicsInputInstruction>{
match &ins.instruction{
Instruction::Input(input_instruction)=>match input_instruction{
&InputInstruction::MoveMouse(m)=>{
if !self.timer.is_paused(){
self.push_mouse_instruction(physics,ins,m);
}
None
},
None=>{
//if mouse0 is never found and the loop ends, we can drain the entire queue
//because we are not waiting for mouse events.
drain_queue(physics,self.queue.drain(..));
break 'outer;
&InputInstruction::MoveForward(s)=>Some(PhysicsInputInstruction::SetMoveForward(s)),
&InputInstruction::MoveLeft(s)=>Some(PhysicsInputInstruction::SetMoveLeft(s)),
&InputInstruction::MoveBack(s)=>Some(PhysicsInputInstruction::SetMoveBack(s)),
&InputInstruction::MoveRight(s)=>Some(PhysicsInputInstruction::SetMoveRight(s)),
&InputInstruction::MoveUp(s)=>Some(PhysicsInputInstruction::SetMoveUp(s)),
&InputInstruction::MoveDown(s)=>Some(PhysicsInputInstruction::SetMoveDown(s)),
&InputInstruction::Jump(s)=>Some(PhysicsInputInstruction::SetJump(s)),
&InputInstruction::Zoom(s)=>Some(PhysicsInputInstruction::SetZoom(s)),
&InputInstruction::Spawn(mode_id,stage_id)=>Some(PhysicsInputInstruction::Spawn(mode_id,stage_id)),
InputInstruction::Restart=>Some(PhysicsInputInstruction::Restart),
InputInstruction::PracticeFly=>Some(PhysicsInputInstruction::PracticeFly),
},
//do these really need to idle the physics?
//sending None dumps the instruction queue
Instruction::GenerateModels(_)=>Some(PhysicsInputInstruction::Idle),
Instruction::ClearModels=>Some(PhysicsInputInstruction::Idle),
Instruction::Resize(_,_)=>Some(PhysicsInputInstruction::Idle),
Instruction::Render=>Some(PhysicsInputInstruction::Idle),
&Instruction::SetPaused(paused)=>{
if let Err(e)=self.timer.set_paused(ins.time,paused){
println!("Cannot pause: {e}");
}
Some(PhysicsInputInstruction::Idle)
},
}
}
fn update_mouse_blocking(&mut self,physics:&crate::physics::PhysicsContext,ins:&TimedInstruction<Instruction>)->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(ins.time)-physics.get_next_mouse().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(ins.time),pos:physics.get_next_mouse().pos}),
});
self.last_mouse_time=self.timer.time(ins.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;
true
}else{
false
}
}else{
//keep this up to date so that it can be used as a known-timestamp
//that the mouse was Timer<Scaled>not moving when the mouse starts moving again
self.last_mouse_time=self.timer.time(ins.time);
true
}
}
/// returns whether or not to empty the instruction queue
fn handle_physics_input(&mut self,physics:&crate::physics::PhysicsContext,ins:&TimedInstruction<Instruction>,phys_input_option:Option<PhysicsInputInstruction>)->bool{
if let Some(phys_input)=phys_input_option{
//non-mouse event
self.timeline.push_back(TimedInstruction{
time:self.timer.time(ins.time),
instruction:phys_input,
});
//this returns the bool for us
self.update_mouse_blocking(physics,ins)
}else{
//mouse event
true
}
}
fn empty_queue(&mut self,physics:&mut crate::physics::PhysicsContext){
while let Some(ins)=self.timeline.pop_front(){
physics.run_input_instruction(ins);
}
}
fn handle_instruction(&mut self,physics:&mut crate::physics::PhysicsContext,ins:&TimedInstruction<Instruction>){
let physics_input_option=self.map_instruction(physics,ins);
let should_empty_queue=self.handle_physics_input(physics,ins,physics_input_option);
if should_empty_queue{
self.empty_queue(physics);
}
}
}
pub fn new(mut physics:crate::physics::PhysicsContext,mut graphics_worker:crate::compat_worker::INWorker<crate::graphics_worker::Instruction>)->crate::compat_worker::QNWorker<TimedInstruction<Instruction>>{
let mut interpolator=MouseInterpolator{
queue:std::collections::VecDeque::new(),
mouse_blocking:true,
last_mouse_time:physics.get_next_mouse().time,
timeline:std::collections::VecDeque::new(),
timer:Timer::from_state(Scaled::identity(),false),
};
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction>|{
let passthrough_instruction=match ins.instruction{
Instruction::Passthrough(passthrough_instruction)=>passthrough_instruction,
Instruction::Interpolate(input_instruction)=>{
interpolator.handle_instruction(&mut physics,TimedInstruction{
instruction:input_instruction,
time:ins.time,
});
return;
interpolator.handle_instruction(&mut physics,&ins);
match ins.instruction{
Instruction::Render=>{
graphics_worker.send(crate::graphics_worker::Instruction::Render(physics.output(),interpolator.timer.time(ins.time),physics.get_next_mouse().pos)).unwrap();
},
};
match passthrough_instruction{
PassthroughInstruction::Render=>{
graphics_worker.send(crate::graphics_worker::Instruction::Render(physics.output(),ins.time,physics.get_next_mouse().pos)).unwrap();
},
PassthroughInstruction::Resize(size,user_settings)=>{
Instruction::Resize(size,user_settings)=>{
graphics_worker.send(crate::graphics_worker::Instruction::Resize(size,user_settings)).unwrap();
},
PassthroughInstruction::GenerateModels(map)=>{
Instruction::GenerateModels(map)=>{
physics.generate_models(&map);
//important!
//bots will not work properly without this exact restart + spawn setup
//reset the physics state to start a new run on the new map
physics.restart();
//generate a spawn event so bots work properly on the first run
//no run started so does not invalidate the run
physics.spawn();
graphics_worker.send(crate::graphics_worker::Instruction::GenerateModels(map)).unwrap();
},
PassthroughInstruction::ClearModels=>{
Instruction::ClearModels=>{
physics.clear();
graphics_worker.send(crate::graphics_worker::Instruction::ClearModels).unwrap();
},
_=>(),
}
})
}

@ -217,9 +217,8 @@ pub fn setup_and_start(title:String){
//the thread that spawns the physics thread
let mut window_thread=window.into_worker(setup_context);
let args:Vec<String>=std::env::args().collect();
if args.len()==2{
let path=std::path::PathBuf::from(&args[1]);
if let Some(arg)=std::env::args().nth(1){
let path=std::path::PathBuf::from(arg);
window_thread.send(TimedInstruction{
time:integer::Time::ZERO,
instruction:WindowInstruction::WindowEvent(winit::event::WindowEvent::DroppedFile(path)),

@ -1,179 +0,0 @@
use strafesnet_common::integer::{Time,Ratio64};
pub trait TimerState:Copy{
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);
}
#[derive(Clone,Copy,Debug)]
struct Scaled{
scale:Ratio64,
offset:Time,
}
impl Scaled{
fn scale(&self,time:Time)->Time{
Time::raw(self.scale.mul_int(time.get()))
}
fn get_scale(&self)->Ratio64{
self.scale
}
fn set_scale(&mut self,time:Time,new_scale:Ratio64){
let new_time=self.get_time(time);
self.scale=new_scale;
self.set_time(time,new_time);
}
}
impl TimerState for Scaled{
fn get_time(&self,time:Time)->Time{
self.scale(time)+self.offset
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-self.scale(time);
}
fn get_offset(&self)->Time{
self.offset
}
fn set_offset(&mut self,offset:Time){
self.offset=offset;
}
}
#[derive(Clone,Copy,Debug)]
struct Realtime{
offset:Time,
}
impl TimerState for Realtime{
fn get_time(&self,time:Time)->Time{
time+self.offset
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-time;
}
fn get_offset(&self)->Time{
self.offset
}
fn set_offset(&mut self,offset:Time){
self.offset=offset;
}
}
#[derive(Clone,Debug)]
pub struct Timer<T>{
state:T,
paused:bool,
}
impl Timer<Realtime>{
pub fn realtime(offset:Time)->Self{
Self{
state:Realtime{offset},
paused:false,
}
}
pub fn realtime_paused(offset:Time)->Self{
Self{
state:Realtime{offset},
paused:true,
}
}
}
#[derive(Debug)]
pub enum Error{
AlreadyPaused,
AlreadyUnpaused,
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for Error{}
impl Timer<Scaled>{
pub fn scaled(scale:Ratio64,offset:Time)->Self{
Self{
state:Scaled{scale,offset},
paused:false,
}
}
pub fn scaled_paused(scale:Ratio64,offset:Time)->Self{
Self{
state:Scaled{scale,offset},
paused:true,
}
}
pub fn get_scale(&mut self)->Ratio64{
self.state.get_scale()
}
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
self.state.set_scale(time,new_scale)
}
}
impl<T:TimerState> Timer<T>{
pub fn time(&self,time:Time)->Time{
match self.paused{
true=>self.state.get_offset(),
false=>self.state.get_time(time),
}
}
pub fn set_time(&mut self,time:Time,new_time:Time){
match self.paused{
true=>self.state.set_offset(new_time),
false=>self.state.set_time(time,new_time),
}
}
pub fn pause(&mut self,time:Time)->Result<(),Error>{
match self.paused{
true=>Err(Error::AlreadyPaused),
false=>{
let new_time=self.time(time);
self.state.set_offset(new_time);
self.paused=true;
Ok(())
},
}
}
pub fn unpause(&mut self,time:Time)->Result<(),Error>{
match self.paused{
true=>{
let new_time=self.time(time);
self.state.set_time(time,new_time);
self.paused=false;
Ok(())
},
false=>Err(Error::AlreadyUnpaused),
}
}
}
#[cfg(test)]
mod test{
use super::{Time,Timer,Error};
macro_rules! sec {
($s: expr) => {
Time::from_secs($s)
};
}
#[test]
fn test_timer()->Result<(),Error>{
//create a paused timer that reads 0s
let mut timer=Timer::realtime_paused(sec!(0));
//the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0));
//unpause it after one second
timer.unpause(sec!(1))?;
//the timer at 6 seconds should read 5s
assert_eq!(timer.time(sec!(6)),sec!(5));
//pause the timer after 11 seconds
timer.pause(sec!(11))?;
//the paused timer at 20 seconds should read 10s
assert_eq!(timer.time(sec!(20)),sec!(10));
Ok(())
}
}

@ -29,18 +29,18 @@ impl WindowContext<'_>{
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::Passthrough(
crate::physics_worker::PassthroughInstruction::ClearModels
)}).unwrap();
self.physics_thread.send(TimedInstruction{time,instruction:crate::physics_worker::Instruction::Passthrough(
crate::physics_worker::PassthroughInstruction::GenerateModels(map)
)}).unwrap();
self.physics_thread.send(TimedInstruction{time,instruction:crate::physics_worker::Instruction::ClearModels}).unwrap();
self.physics_thread.send(TimedInstruction{time,instruction:crate::physics_worker::Instruction::GenerateModels(map)}).unwrap();
},
Err(e)=>println!("Failed to load map: {e}"),
}
},
winit::event::WindowEvent::Focused(_state)=>{
winit::event::WindowEvent::Focused(state)=>{
//pause unpause
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::SetPaused(!state),
}).unwrap();
//recalculate pressed keys on focus
},
winit::event::WindowEvent::KeyboardInput{
@ -111,7 +111,11 @@ impl WindowContext<'_>{
"e"=>Some(InputInstruction::MoveUp(s)),
"q"=>Some(InputInstruction::MoveDown(s)),
"z"=>Some(InputInstruction::Zoom(s)),
"r"=>if s{Some(InputInstruction::Reset)}else{None},
"r"=>if s{
//mouse needs to be reset since the position is absolute
self.mouse=crate::physics::MouseState::default();
Some(InputInstruction::Restart)
}else{None},
"f"=>if s{Some(InputInstruction::PracticeFly)}else{None},
_=>None,
},
@ -119,7 +123,7 @@ impl WindowContext<'_>{
}{
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Interpolate(input_instruction),
instruction:crate::physics_worker::Instruction::Input(input_instruction),
}).unwrap();
}
},
@ -147,7 +151,7 @@ impl WindowContext<'_>{
self.mouse.pos+=delta;
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Interpolate(InputInstruction::MoveMouse(self.mouse.pos)),
instruction:crate::physics_worker::Instruction::Input(InputInstruction::MoveMouse(self.mouse.pos)),
}).unwrap();
},
winit::event::DeviceEvent::MouseWheel {
@ -157,7 +161,7 @@ impl WindowContext<'_>{
if false{//self.physics.style.use_scroll{
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Interpolate(InputInstruction::Jump(true)),//activates the immediate jump path, but the style modifier prevents controls&CONTROL_JUMP bit from being set to auto jump
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
}).unwrap();
}
}
@ -222,9 +226,7 @@ impl<'a> WindowContextSetup<'a>{
window_context.physics_thread.send(
TimedInstruction{
time:ins.time,
instruction:crate::physics_worker::Instruction::Passthrough(
crate::physics_worker::PassthroughInstruction::Resize(size,window_context.user_settings.clone())
)
instruction:crate::physics_worker::Instruction::Resize(size,window_context.user_settings.clone())
}
).unwrap();
}
@ -232,9 +234,7 @@ impl<'a> WindowContextSetup<'a>{
window_context.physics_thread.send(
TimedInstruction{
time:ins.time,
instruction:crate::physics_worker::Instruction::Passthrough(
crate::physics_worker::PassthroughInstruction::Render
)
instruction:crate::physics_worker::Instruction::Render
}
).unwrap();
}

@ -190,7 +190,7 @@ mod test{
for _ in 0..5 {
let task = instruction::TimedInstruction{
time:integer::Time::ZERO,
instruction:physics::PhysicsInstruction::StrafeTick,
instruction:physics::PhysicsInputInstruction::Idle,
};
worker.send(task).unwrap();
}
@ -204,7 +204,7 @@ mod test{
// Send a new task
let task = instruction::TimedInstruction{
time:integer::Time::ZERO,
instruction:physics::PhysicsInstruction::StrafeTick,
instruction:physics::PhysicsInputInstruction::Idle,
};
worker.send(task).unwrap();

1
tools/iso Executable file

@ -0,0 +1 @@
mangohud ../target/release/strafe-client bhop_maps/5692124338.snfm