Compare commits

..

16 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
10 changed files with 288 additions and 304 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)]
@ -249,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]
@ -558,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
@ -574,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
@ -768,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
@ -777,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})
),
}
@ -790,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})
),
}
@ -906,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{
@ -925,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(),
}
}
}
@ -944,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
@ -1025,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{
@ -1065,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();
@ -1132,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),
@ -1149,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);
@ -1171,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}),
})}
@ -1242,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))
}
@ -1298,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))
}
@ -1307,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))=>{
@ -1411,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
@ -1438,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;
@ -1460,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
@ -1484,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);
@ -1503,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{
@ -1512,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{
@ -1541,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,7 +14,8 @@ 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{
@ -21,12 +24,14 @@ pub enum Instruction{
Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings),
GenerateModels(strafesnet_common::map::CompleteMap),
ClearModels,
SetPaused(bool),
//Graphics(crate::graphics_worker::Instruction),
}
pub struct MouseInterpolator{
timeline:std::collections::VecDeque<TimedInstruction<PhysicsInputInstruction>>,
last_mouse_time:Time,
last_mouse_time:Time,//this value is pre-transformed to simulation time
mouse_blocking:bool,
timer:Timer<Scaled>,
}
impl MouseInterpolator{
fn push_mouse_instruction(&mut self,physics:&crate::physics::PhysicsContext,ins:&TimedInstruction<Instruction>,m:glam::IVec2){
@ -34,7 +39,7 @@ impl MouseInterpolator{
//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:ins.time,pos:m}),
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.
@ -43,13 +48,13 @@ impl MouseInterpolator{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::ReplaceMouse(
MouseState{time:self.last_mouse_time,pos:physics.get_next_mouse().pos},
MouseState{time:ins.time,pos:m}
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=ins.time;
self.last_mouse_time=self.timer.time(ins.time);
}
/// returns the mapped physics input instruction
/// may or may not mutate internal state XD!
@ -57,7 +62,9 @@ impl MouseInterpolator{
match &ins.instruction{
Instruction::Input(input_instruction)=>match input_instruction{
&InputInstruction::MoveMouse(m)=>{
self.push_mouse_instruction(physics,ins,m);
if !self.timer.is_paused(){
self.push_mouse_instruction(physics,ins,m);
}
None
},
&InputInstruction::MoveForward(s)=>Some(PhysicsInputInstruction::SetMoveForward(s)),
@ -68,13 +75,22 @@ impl MouseInterpolator{
&InputInstruction::MoveDown(s)=>Some(PhysicsInputInstruction::SetMoveDown(s)),
&InputInstruction::Jump(s)=>Some(PhysicsInputInstruction::SetJump(s)),
&InputInstruction::Zoom(s)=>Some(PhysicsInputInstruction::SetZoom(s)),
InputInstruction::Reset=>Some(PhysicsInputInstruction::Reset),
&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{
@ -83,13 +99,13 @@ impl MouseInterpolator{
//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)<ins.time-physics.get_next_mouse().time{
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:ins.time,pos:physics.get_next_mouse().pos}),
instruction:PhysicsInputInstruction::SetNextMouse(MouseState{time:self.timer.time(ins.time),pos:physics.get_next_mouse().pos}),
});
self.last_mouse_time=ins.time;
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
@ -98,8 +114,8 @@ impl MouseInterpolator{
}
}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=ins.time;
//that the mouse was Timer<Scaled>not moving when the mouse starts moving again
self.last_mouse_time=self.timer.time(ins.time);
true
}
}
@ -108,7 +124,7 @@ impl MouseInterpolator{
if let Some(phys_input)=phys_input_option{
//non-mouse event
self.timeline.push_back(TimedInstruction{
time:ins.time,
time:self.timer.time(ins.time),
instruction:phys_input,
});
@ -120,8 +136,8 @@ impl MouseInterpolator{
}
}
fn empty_queue(&mut self,physics:&mut crate::physics::PhysicsContext){
while let Some(instruction)=self.timeline.pop_front(){
physics.run_input_instruction(instruction);
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>){
@ -138,18 +154,25 @@ pub fn new(mut physics:crate::physics::PhysicsContext,mut graphics_worker:crate:
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>|{
interpolator.handle_instruction(&mut physics,&ins);
match ins.instruction{
Instruction::Render=>{
graphics_worker.send(crate::graphics_worker::Instruction::Render(physics.output(),ins.time,physics.get_next_mouse().pos)).unwrap();
graphics_worker.send(crate::graphics_worker::Instruction::Render(physics.output(),interpolator.timer.time(ins.time),physics.get_next_mouse().pos)).unwrap();
},
Instruction::Resize(size,user_settings)=>{
graphics_worker.send(crate::graphics_worker::Instruction::Resize(size,user_settings)).unwrap();
},
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();
},

@ -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,184 +0,0 @@
use strafesnet_common::integer::{Time,Ratio64};
//this could be about half as long if I only had
//scaled timers and just used a scale of 1
//but I thought the concept of a timer that could
//only be paused and not scaled was cool
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(())
}
}

@ -35,8 +35,12 @@ impl WindowContext<'_>{
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{
@ -107,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,
},
@ -233,4 +241,4 @@ impl<'a> WindowContextSetup<'a>{
}
})
}
}
}

@ -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