forked from StrafesNET/strafe-client
wip
This commit is contained in:
parent
35a0d0d655
commit
9f2c3cd242
325
src/physics.rs
325
src/physics.rs
@ -1,9 +1,17 @@
|
|||||||
use crate::model_physics::{PhysicsMesh,TransformedMesh,MeshQuery};
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::model_physics::{self,PhysicsMesh,TransformedMesh,MeshQuery};
|
||||||
use strafesnet_common::bvh;
|
use strafesnet_common::bvh;
|
||||||
use strafesnet_common::aabb;
|
use strafesnet_common::aabb;
|
||||||
use strafesnet_common::game_mechanics::{self,StyleModifiers};
|
use strafesnet_common::gameplay_attributes;
|
||||||
|
use strafesnet_common::gameplay_modes;
|
||||||
|
use strafesnet_common::map;
|
||||||
|
use strafesnet_common::model;
|
||||||
|
use strafesnet_common::gameplay_style::{self,StyleModifiers,StrafeSettings};
|
||||||
use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer,TimedInstruction};
|
use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer,TimedInstruction};
|
||||||
use strafesnet_common::integer::{self,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2};
|
use strafesnet_common::integer::{self,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2};
|
||||||
|
use strafesnet_common::model::ModelId;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PhysicsInstruction {
|
pub enum PhysicsInstruction {
|
||||||
@ -153,18 +161,16 @@ struct PhysicsModels{
|
|||||||
//attributes can be split into contacting and intersecting (this also saves a bit of memory)
|
//attributes can be split into contacting and intersecting (this also saves a bit of memory)
|
||||||
//can go even further and deduplicate General attributes separately, reconstructing it when queried
|
//can go even further and deduplicate General attributes separately, reconstructing it when queried
|
||||||
attributes:Vec<PhysicsCollisionAttributes>,
|
attributes:Vec<PhysicsCollisionAttributes>,
|
||||||
model_id_from_wormhole_id:std::collections::HashMap::<u32,usize>,
|
|
||||||
}
|
}
|
||||||
impl PhysicsModels{
|
impl PhysicsModels{
|
||||||
fn clear(&mut self){
|
fn clear(&mut self){
|
||||||
self.meshes.clear();
|
self.meshes.clear();
|
||||||
self.models.clear();
|
self.models.clear();
|
||||||
self.attributes.clear();
|
self.attributes.clear();
|
||||||
self.model_id_from_wormhole_id.clear();
|
|
||||||
}
|
}
|
||||||
//TODO: "statically" verify the maps don't refer to any nonexistant data, if they do delete the references.
|
//TODO: "statically" verify the maps don't refer to any nonexistant data, if they do delete the references.
|
||||||
//then I can make these getter functions unchecked.
|
//then I can make these getter functions unchecked.
|
||||||
fn mesh(&self,model_id:usize)->TransformedMesh{
|
fn mesh(&self,model_id:ModelId)->TransformedMesh{
|
||||||
TransformedMesh::new(
|
TransformedMesh::new(
|
||||||
&self.meshes[self.models[model_id].mesh_id],
|
&self.meshes[self.models[model_id].mesh_id],
|
||||||
&self.models[model_id].transform,
|
&self.models[model_id].transform,
|
||||||
@ -172,24 +178,21 @@ impl PhysicsModels{
|
|||||||
self.models[model_id].transform_det,
|
self.models[model_id].transform_det,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn model(&self,model_id:usize)->&PhysicsModel{
|
fn model(&self,model_id:ModelId)->&PhysicsModel{
|
||||||
&self.models[model_id]
|
&self.models[model_id]
|
||||||
}
|
}
|
||||||
fn attr(&self,model_id:usize)->&PhysicsCollisionAttributes{
|
fn attr(&self,model_id:ModelId)->&PhysicsCollisionAttributes{
|
||||||
&self.attributes[self.models[model_id].attr_id]
|
&self.attributes[self.models[model_id].attr_id]
|
||||||
}
|
}
|
||||||
fn get_wormhole_model(&self,wormhole_id:u32)->Option<&PhysicsModel>{
|
|
||||||
self.models.get(*self.model_id_from_wormhole_id.get(&wormhole_id)?)
|
|
||||||
}
|
|
||||||
fn push_mesh(&mut self,mesh:PhysicsMesh){
|
fn push_mesh(&mut self,mesh:PhysicsMesh){
|
||||||
self.meshes.push(mesh);
|
self.meshes.push(mesh);
|
||||||
}
|
}
|
||||||
fn push_model(&mut self,model:PhysicsModel)->usize{
|
fn push_model(&mut self,model:PhysicsModel)->ModelId{
|
||||||
let model_id=self.models.len();
|
let model_id=self.models.len();
|
||||||
self.models.push(model);
|
self.models.push(model);
|
||||||
model_id
|
model_id
|
||||||
}
|
}
|
||||||
fn push_attr(&mut self,attr:PhysicsCollisionAttributes)->usize{
|
fn push_attr(&mut self,attr:PhysicsCollisionAttributes)->PhysicsAttributeId{
|
||||||
let attr_id=self.attributes.len();
|
let attr_id=self.attributes.len();
|
||||||
self.attributes.push(attr);
|
self.attributes.push(attr);
|
||||||
attr_id
|
attr_id
|
||||||
@ -257,17 +260,17 @@ impl std::default::Default for PhysicsCamera{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GameMechanicsState{
|
pub struct ModeState{
|
||||||
stage_id:u32,
|
stage_id:gameplay_modes::StageId,
|
||||||
jump_counts:std::collections::HashMap<usize,u32>,//model_id -> jump count
|
jump_counts:HashMap<ModelId,u32>,//model_id -> jump count
|
||||||
next_ordered_checkpoint_id:u32,//which OrderedCheckpoint model_id you must pass next (if 0 you haven't passed OrderedCheckpoint0)
|
next_ordered_checkpoint_id:gameplay_modes::CheckpointId,//which OrderedCheckpoint model_id you must pass next (if 0 you haven't passed OrderedCheckpoint0)
|
||||||
unordered_checkpoints:std::collections::HashSet<usize>,//hashset of UnorderedCheckpoint model ids
|
unordered_checkpoints:HashSet<ModelId>,
|
||||||
}
|
}
|
||||||
impl std::default::Default for GameMechanicsState{
|
impl std::default::Default for ModeState{
|
||||||
fn default()->Self{
|
fn default()->Self{
|
||||||
Self{
|
Self{
|
||||||
stage_id:0,
|
stage_id:gameplay_modes::StageId::id(0),
|
||||||
next_ordered_checkpoint_id:0,
|
next_ordered_checkpoint_id:gameplay_modes::CheckpointId::id(0),
|
||||||
unordered_checkpoints:std::collections::HashSet::new(),
|
unordered_checkpoints:std::collections::HashSet::new(),
|
||||||
jump_counts:std::collections::HashMap::new(),
|
jump_counts:std::collections::HashMap::new(),
|
||||||
}
|
}
|
||||||
@ -280,7 +283,7 @@ trait HitboxMeshPresets{
|
|||||||
fn roblox()->Self;
|
fn roblox()->Self;
|
||||||
fn source()->Self;
|
fn source()->Self;
|
||||||
}
|
}
|
||||||
impl HitboxMeshPresets for game_mechanics::Hitbox{
|
impl HitboxMeshPresets for gameplay_style::Hitbox{
|
||||||
fn roblox()->Self{
|
fn roblox()->Self{
|
||||||
Self::from_mesh_scale(PhysicsMesh::from(&crate::primitives::unit_cylinder()),Planar64Vec3::int(2,5,2)/2)
|
Self::from_mesh_scale(PhysicsMesh::from(&crate::primitives::unit_cylinder()),Planar64Vec3::int(2,5,2)/2)
|
||||||
}
|
}
|
||||||
@ -309,12 +312,7 @@ impl StyleHelper for StyleModifiers{
|
|||||||
|
|
||||||
fn allow_strafe(&self,controls:u32)->bool{
|
fn allow_strafe(&self,controls:u32)->bool{
|
||||||
//disable strafing according to strafe settings
|
//disable strafing according to strafe settings
|
||||||
match &self.strafe{
|
self.strafe.is_some_and(|s|s.mask(controls))
|
||||||
Some(StrafeSettings{enable:EnableStrafe::Always,air_accel_limit:_,tick_rate:_})=>true,
|
|
||||||
&Some(StrafeSettings{enable:EnableStrafe::MaskAny(mask),air_accel_limit:_,tick_rate:_})=>mask&controls!=0,
|
|
||||||
&Some(StrafeSettings{enable:EnableStrafe::MaskAll(mask),air_accel_limit:_,tick_rate:_})=>mask&controls==mask,
|
|
||||||
None=>false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_control_dir(&self,controls:u32)->Planar64Vec3{
|
fn get_control_dir(&self,controls:u32)->Planar64Vec3{
|
||||||
@ -429,21 +427,18 @@ pub struct PhysicsState{
|
|||||||
time:Time,
|
time:Time,
|
||||||
body:Body,
|
body:Body,
|
||||||
world:WorldState,//currently there is only one state the world can be in
|
world:WorldState,//currently there is only one state the world can be in
|
||||||
game:GameMechanicsState,
|
mode_state:ModeState,
|
||||||
style:StyleModifiers,
|
style:StyleModifiers,//mode style with custom style updates applied
|
||||||
touching:TouchingState,
|
touching:TouchingState,
|
||||||
//camera must exist in state because wormholes modify the camera, also camera punch
|
//camera must exist in state because wormholes modify the camera, also camera punch
|
||||||
camera:PhysicsCamera,
|
camera:PhysicsCamera,
|
||||||
pub next_mouse:MouseState,//Where is the mouse headed next
|
pub next_mouse:MouseState,//Where is the mouse headed next
|
||||||
controls:u32,
|
controls:u32,//TODO this should be a struct
|
||||||
move_state:MoveState,
|
move_state:MoveState,
|
||||||
models:PhysicsModels,
|
//does not belong here
|
||||||
bvh:bvh::BvhNode,
|
//bvh:bvh::BvhNode,
|
||||||
|
//models:PhysicsModels,
|
||||||
modes:Modes,
|
//modes:gameplay_modes::Modes,
|
||||||
//the spawn point is where you spawn when you load into the map.
|
|
||||||
//This is not the same as Reset which teleports you to Spawn0
|
|
||||||
spawn_point:Planar64Vec3,
|
|
||||||
}
|
}
|
||||||
#[derive(Clone,Default)]
|
#[derive(Clone,Default)]
|
||||||
pub struct PhysicsOutputState{
|
pub struct PhysicsOutputState{
|
||||||
@ -460,38 +455,53 @@ impl PhysicsOutputState{
|
|||||||
#[derive(Clone,Hash,Eq,PartialEq)]
|
#[derive(Clone,Hash,Eq,PartialEq)]
|
||||||
enum PhysicsCollisionAttributes{
|
enum PhysicsCollisionAttributes{
|
||||||
Contact{//track whether you are contacting the object
|
Contact{//track whether you are contacting the object
|
||||||
contacting:crate::model::ContactingAttributes,
|
contacting:gameplay_attributes::ContactingAttributes,
|
||||||
general:crate::model::GameMechanicAttributes,
|
general:gameplay_attributes::GeneralAttributes,
|
||||||
},
|
},
|
||||||
Intersect{//track whether you are intersecting the object
|
Intersect{//track whether you are intersecting the object
|
||||||
intersecting:crate::model::IntersectingAttributes,
|
intersecting:gameplay_attributes::IntersectingAttributes,
|
||||||
general:crate::model::GameMechanicAttributes,
|
general:gameplay_attributes::GeneralAttributes,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
struct NonPhysicsError;
|
struct NonPhysicsError;
|
||||||
impl TryFrom<&crate::model::CollisionAttributes> for PhysicsCollisionAttributes{
|
impl TryFrom<&gameplay_attributes::CollisionAttributes> for PhysicsCollisionAttributes{
|
||||||
type Error=NonPhysicsError;
|
type Error=NonPhysicsError;
|
||||||
fn try_from(value:&crate::model::CollisionAttributes)->Result<Self,Self::Error>{
|
fn try_from(value:&gameplay_attributes::CollisionAttributes)->Result<Self,Self::Error>{
|
||||||
match value{
|
match value{
|
||||||
crate::model::CollisionAttributes::Decoration=>Err(NonPhysicsError),
|
gameplay_attributes::CollisionAttributes::Decoration=>Err(NonPhysicsError),
|
||||||
crate::model::CollisionAttributes::Contact{contacting,general}=>Ok(Self::Contact{contacting:contacting.clone(),general:general.clone()}),
|
gameplay_attributes::CollisionAttributes::Contact{contacting,general}=>Ok(Self::Contact{contacting:contacting.clone(),general:general.clone()}),
|
||||||
crate::model::CollisionAttributes::Intersect{intersecting,general}=>Ok(Self::Intersect{intersecting:intersecting.clone(),general:general.clone()}),
|
gameplay_attributes::CollisionAttributes::Intersect{intersecting,general}=>Ok(Self::Intersect{intersecting:intersecting.clone(),general:general.clone()}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct GeneralAttributesId(u32);
|
||||||
|
struct ContactAttributesId(u32);
|
||||||
|
struct IntersectAttributesId(u32);
|
||||||
|
enum PhysicsAttributesId{
|
||||||
|
Contact(ContactAttributesId),
|
||||||
|
Intersect(IntersectAttributesId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//unique physics meshes indexed by this
|
||||||
|
struct MeshId{
|
||||||
|
model_id:ModelId,
|
||||||
|
group_id:GroupId,
|
||||||
|
}
|
||||||
pub struct PhysicsModel{
|
pub struct PhysicsModel{
|
||||||
//A model is a thing that has a hitbox. can be represented by a list of TreyMesh-es
|
//A model is a thing that has a hitbox. can be represented by a list of TreyMesh-es
|
||||||
//in this iteration, all it needs is extents.
|
//in this iteration, all it needs is extents.
|
||||||
mesh_id:usize,
|
mesh_id:MeshId,
|
||||||
attr_id:usize,
|
//put these up on the Model (data normalization)
|
||||||
|
general_attributes:GeneralAttributesId,
|
||||||
|
collision_attributes:PhysicsAttributesId,
|
||||||
transform:integer::Planar64Affine3,
|
transform:integer::Planar64Affine3,
|
||||||
normal_transform:integer::Planar64Mat3,
|
normal_transform:integer::Planar64Mat3,
|
||||||
transform_det:Planar64,
|
transform_det:Planar64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PhysicsModel{
|
impl PhysicsModel{
|
||||||
pub fn new(mesh_id:usize,attr_id:usize,transform:integer::Planar64Affine3)->Self{
|
pub fn new(mesh_id:MeshId,attr_id:PhysicsAttributesId,transform:integer::Planar64Affine3)->Self{
|
||||||
Self{
|
Self{
|
||||||
mesh_id,
|
mesh_id,
|
||||||
attr_id,
|
attr_id,
|
||||||
@ -504,12 +514,12 @@ impl PhysicsModel{
|
|||||||
|
|
||||||
#[derive(Debug,Clone,Eq,Hash,PartialEq)]
|
#[derive(Debug,Clone,Eq,Hash,PartialEq)]
|
||||||
struct ContactCollision{
|
struct ContactCollision{
|
||||||
face_id:crate::model_physics::MinkowskiFace,
|
face_id:model_physics::MinkowskiFace,
|
||||||
model_id:usize,//using id to avoid lifetimes
|
model_id:ModelId,//using id to avoid lifetimes
|
||||||
}
|
}
|
||||||
#[derive(Debug,Clone,Eq,Hash,PartialEq)]
|
#[derive(Debug,Clone,Eq,Hash,PartialEq)]
|
||||||
struct IntersectCollision{
|
struct IntersectCollision{
|
||||||
model_id:usize,
|
model_id:ModelId,
|
||||||
}
|
}
|
||||||
#[derive(Debug,Clone,Eq,Hash,PartialEq)]
|
#[derive(Debug,Clone,Eq,Hash,PartialEq)]
|
||||||
enum Collision{
|
enum Collision{
|
||||||
@ -517,13 +527,13 @@ enum Collision{
|
|||||||
Intersect(IntersectCollision),
|
Intersect(IntersectCollision),
|
||||||
}
|
}
|
||||||
impl Collision{
|
impl Collision{
|
||||||
fn model_id(&self)->usize{
|
fn model_id(&self)->ModelId{
|
||||||
match self{
|
match self{
|
||||||
&Collision::Contact(ContactCollision{model_id,face_id:_})
|
&Collision::Contact(ContactCollision{model_id,face_id:_})
|
||||||
|&Collision::Intersect(IntersectCollision{model_id})=>model_id,
|
|&Collision::Intersect(IntersectCollision{model_id})=>model_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn face_id(&self)->Option<crate::model_physics::MinkowskiFace>{
|
fn face_id(&self)->Option<model_physics::MinkowskiFace>{
|
||||||
match self{
|
match self{
|
||||||
&Collision::Contact(ContactCollision{model_id:_,face_id})=>Some(face_id),
|
&Collision::Contact(ContactCollision{model_id:_,face_id})=>Some(face_id),
|
||||||
&Collision::Intersect(IntersectCollision{model_id:_})=>None,
|
&Collision::Intersect(IntersectCollision{model_id:_})=>None,
|
||||||
@ -616,7 +626,7 @@ impl TouchingState{
|
|||||||
PhysicsCollisionAttributes::Contact{contacting,general}=>{
|
PhysicsCollisionAttributes::Contact{contacting,general}=>{
|
||||||
let normal=contact_normal(models,&style_mesh,contact);
|
let normal=contact_normal(models,&style_mesh,contact);
|
||||||
match &contacting.contact_behaviour{
|
match &contacting.contact_behaviour{
|
||||||
Some(crate::model::ContactingBehaviour::Ladder(_))=>{
|
Some(model::ContactingBehaviour::Ladder(_))=>{
|
||||||
//ladder walkstate
|
//ladder walkstate
|
||||||
let mut target_velocity=style.get_ladder_target_velocity(camera,controls,next_mouse,time,&normal);
|
let mut target_velocity=style.get_ladder_target_velocity(camera,controls,next_mouse,time,&normal);
|
||||||
self.constrain_velocity(models,&style_mesh,&mut target_velocity);
|
self.constrain_velocity(models,&style_mesh,&mut target_velocity);
|
||||||
@ -651,7 +661,7 @@ impl TouchingState{
|
|||||||
for contact in &self.contacts{
|
for contact in &self.contacts{
|
||||||
//detect face slide off
|
//detect face slide off
|
||||||
let model_mesh=models.mesh(contact.model_id);
|
let model_mesh=models.mesh(contact.model_id);
|
||||||
let minkowski=crate::model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,&style_mesh);
|
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,&style_mesh);
|
||||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(face,time)|{
|
collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(face,time)|{
|
||||||
TimedInstruction{
|
TimedInstruction{
|
||||||
time,
|
time,
|
||||||
@ -664,7 +674,7 @@ impl TouchingState{
|
|||||||
for intersect in &self.intersects{
|
for intersect in &self.intersects{
|
||||||
//detect model collision in reverse
|
//detect model collision in reverse
|
||||||
let model_mesh=models.mesh(intersect.model_id);
|
let model_mesh=models.mesh(intersect.model_id);
|
||||||
let minkowski=crate::model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,&style_mesh);
|
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,&style_mesh);
|
||||||
collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(face,time)|{
|
collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(face,time)|{
|
||||||
TimedInstruction{
|
TimedInstruction{
|
||||||
time,
|
time,
|
||||||
@ -784,7 +794,7 @@ impl Default for PhysicsState{
|
|||||||
next_mouse:MouseState::default(),
|
next_mouse:MouseState::default(),
|
||||||
controls:0,
|
controls:0,
|
||||||
world:WorldState{},
|
world:WorldState{},
|
||||||
game:GameMechanicsState::default(),
|
mode_state:ModeState::default(),
|
||||||
modes:Modes::default(),
|
modes:Modes::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -807,7 +817,7 @@ impl PhysicsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(&mut self,spawn_point:Planar64Vec3){
|
pub fn spawn(&mut self,spawn_point:Planar64Vec3){
|
||||||
self.game.stage_id=0;
|
self.mode_state.stage_id=0;
|
||||||
self.spawn_point=spawn_point;
|
self.spawn_point=spawn_point;
|
||||||
self.process_instruction(instruction::TimedInstruction{
|
self.process_instruction(instruction::TimedInstruction{
|
||||||
time:self.time,
|
time:self.time,
|
||||||
@ -815,7 +825,7 @@ impl PhysicsState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_models(&mut self,indexed_models:&crate::model::IndexedModelInstances){
|
pub fn generate_models(&mut self,indexed_models:&map::Map){
|
||||||
let mut starts=Vec::new();
|
let mut starts=Vec::new();
|
||||||
let mut spawns=Vec::new();
|
let mut spawns=Vec::new();
|
||||||
let mut attr_hash=std::collections::HashMap::new();
|
let mut attr_hash=std::collections::HashMap::new();
|
||||||
@ -833,14 +843,7 @@ impl PhysicsState {
|
|||||||
};
|
};
|
||||||
let model_physics=PhysicsModel::new(mesh_id,attr_id,model_instance.transform);
|
let model_physics=PhysicsModel::new(mesh_id,attr_id,model_instance.transform);
|
||||||
make_mesh=true;
|
make_mesh=true;
|
||||||
let model_id=self.models.push_model(model_physics);
|
self.models.push_model(model_physics);
|
||||||
for attr in &model_instance.temp_indexing{
|
|
||||||
match attr{
|
|
||||||
crate::model::TempIndexedAttributes::Start(s)=>starts.push((model_id,s.clone())),
|
|
||||||
crate::model::TempIndexedAttributes::Spawn(s)=>spawns.push((model_id,s.clone())),
|
|
||||||
crate::model::TempIndexedAttributes::Wormhole(s)=>{self.models.model_id_from_wormhole_id.insert(s.wormhole_id,model_id);},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if make_mesh{
|
if make_mesh{
|
||||||
@ -848,32 +851,6 @@ impl PhysicsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.bvh=bvh::generate_bvh(self.models.aabb_list());
|
self.bvh=bvh::generate_bvh(self.models.aabb_list());
|
||||||
//I don't wanna write structs for temporary structures
|
|
||||||
//this code builds ModeDescriptions from the unsorted lists at the top of the function
|
|
||||||
starts.sort_by_key(|tup|tup.1.mode_id);
|
|
||||||
let mut mode_id_from_map_mode_id=std::collections::HashMap::new();
|
|
||||||
let mut modedatas:Vec<(usize,Vec<(u32,usize)>,u32)>=starts.into_iter().enumerate().map(|(i,(model_id,s))|{
|
|
||||||
mode_id_from_map_mode_id.insert(s.mode_id,i);
|
|
||||||
(model_id,Vec::new(),s.mode_id)
|
|
||||||
}).collect();
|
|
||||||
for (model_id,s) in spawns{
|
|
||||||
if let Some(mode_id)=mode_id_from_map_mode_id.get(&s.mode_id){
|
|
||||||
if let Some(modedata)=modedatas.get_mut(*mode_id){
|
|
||||||
modedata.1.push((s.stage_id,model_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for mut tup in modedatas.into_iter(){
|
|
||||||
tup.1.sort_by_key(|tup|tup.0);
|
|
||||||
let mut eshmep1=std::collections::HashMap::new();
|
|
||||||
let mut eshmep2=std::collections::HashMap::new();
|
|
||||||
self.modes.insert(tup.2,crate::model::ModeDescription{
|
|
||||||
start:tup.0,
|
|
||||||
spawns:tup.1.into_iter().enumerate().map(|(i,tup)|{eshmep1.insert(tup.0,i);tup.1}).collect(),
|
|
||||||
spawn_from_stage_id:eshmep1,
|
|
||||||
ordered_checkpoint_from_checkpoint_id:eshmep2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
println!("Physics Objects: {}",self.models.models.len());
|
println!("Physics Objects: {}",self.models.models.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -903,7 +880,7 @@ impl PhysicsState {
|
|||||||
fn next_strafe_instruction(&self)->Option<TimedInstruction<PhysicsInstruction>>{
|
fn next_strafe_instruction(&self)->Option<TimedInstruction<PhysicsInstruction>>{
|
||||||
self.style.strafe.as_ref().map(|strafe|{
|
self.style.strafe.as_ref().map(|strafe|{
|
||||||
TimedInstruction{
|
TimedInstruction{
|
||||||
time:Time::from_nanos(strafe.tick_rate.rhs_div_int(strafe.tick_rate.mul_int(self.time.nanos())+1)),
|
time:strafe.next_tick(self.time),
|
||||||
//only poll the physics if there is a before and after mouse event
|
//only poll the physics if there is a before and after mouse event
|
||||||
instruction:PhysicsInstruction::StrafeTick
|
instruction:PhysicsInstruction::StrafeTick
|
||||||
}
|
}
|
||||||
@ -1003,7 +980,7 @@ impl instruction::InstructionEmitter<PhysicsInstruction> for PhysicsState{
|
|||||||
self.bvh.the_tester(&aabb,&mut |id|{
|
self.bvh.the_tester(&aabb,&mut |id|{
|
||||||
//no checks are needed because of the time limits.
|
//no checks are needed because of the time limits.
|
||||||
let model_mesh=self.models.mesh(id);
|
let model_mesh=self.models.mesh(id);
|
||||||
let minkowski=crate::model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,&style_mesh);
|
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,&style_mesh);
|
||||||
collector.collect(minkowski.predict_collision_in(&relative_body,collector.time())
|
collector.collect(minkowski.predict_collision_in(&relative_body,collector.time())
|
||||||
//temp (?) code to avoid collision loops
|
//temp (?) code to avoid collision loops
|
||||||
.map_or(None,|(face,time)|if time==self.time{None}else{Some((face,time))})
|
.map_or(None,|(face,time)|if time==self.time{None}else{Some((face,time))})
|
||||||
@ -1035,7 +1012,7 @@ fn jumped_velocity(models:&PhysicsModels,style:&StyleModifiers,walk_state:&WalkS
|
|||||||
|
|
||||||
fn contact_normal(models:&PhysicsModels,style_mesh:&TransformedMesh,contact:&ContactCollision)->Planar64Vec3{
|
fn contact_normal(models:&PhysicsModels,style_mesh:&TransformedMesh,contact:&ContactCollision)->Planar64Vec3{
|
||||||
let model_mesh=models.mesh(contact.model_id);
|
let model_mesh=models.mesh(contact.model_id);
|
||||||
let minkowski=crate::model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,style_mesh);
|
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(&model_mesh,style_mesh);
|
||||||
minkowski.face_nd(contact.face_id).0
|
minkowski.face_nd(contact.face_id).0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1095,75 +1072,75 @@ fn teleport(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,sty
|
|||||||
set_acceleration(body,touching,models,&style.mesh(),style.gravity);
|
set_acceleration(body,touching,models,&style.mesh(),style.gravity);
|
||||||
MoveState::Air
|
MoveState::Air
|
||||||
}
|
}
|
||||||
fn teleport_to_spawn(body:&mut Body,touching:&mut TouchingState,style:&StyleModifiers,mode:&crate::model::ModeDescription,models:&PhysicsModels,stage_id:u32)->Option<MoveState>{
|
fn teleport_to_spawn(body:&mut Body,touching:&mut TouchingState,style:&StyleModifiers,mode:&gameplay_modes::Mode,models:&PhysicsModels,stage_id:StageId)->Option<MoveState>{
|
||||||
let model=models.model(*mode.get_spawn_model_id(stage_id)? as usize);
|
let model=models.model(mode.get_spawn_model_id(stage_id)?);
|
||||||
let point=model.transform.transform_point3(Planar64Vec3::Y)+Planar64Vec3::Y*(style.hitbox.halfsize.y()+Planar64::ONE/16);
|
let point=model.transform.transform_point3(Planar64Vec3::Y)+Planar64Vec3::Y*(style.hitbox.halfsize.y()+Planar64::ONE/16);
|
||||||
Some(teleport(body,touching,models,style,point))
|
Some(teleport(body,touching,models,style,point))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_teleport_behaviour(teleport_behaviour:&Option<crate::model::TeleportBehaviour>,game:&mut GameMechanicsState,models:&PhysicsModels,modes:&Modes,style:&StyleModifiers,touching:&mut TouchingState,body:&mut Body,model_id:usize)->Option<MoveState>{
|
fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,game:&mut ModeState,models:&PhysicsModels,mode:&gameplay_modes::Mode,style:&StyleModifiers,touching:&mut TouchingState,body:&mut Body,model_id:ModelId)->Option<MoveState>{
|
||||||
//TODO: jump count and checkpoints are always reset on teleport.
|
//TODO: jump count and checkpoints are always reset on teleport.
|
||||||
//Map makers are expected to use tools to prevent
|
//Map makers are expected to use tools to prevent
|
||||||
//multi-boosting on JumpLimit boosters such as spawning into a SetVelocity
|
//multi-boosting on JumpLimit boosters such as spawning into a SetVelocity
|
||||||
match teleport_behaviour{
|
mode.get_element(model_id).map(|stage_element|{
|
||||||
Some(crate::model::TeleportBehaviour::StageElement(stage_element))=>{
|
if stage_element.force||game.stage_id<stage_element.stage_id{
|
||||||
if stage_element.force||game.stage_id<stage_element.stage_id{
|
//TODO: check if all checkpoints are complete up to destination stage id, otherwise set to checkpoint completion stage it
|
||||||
//TODO: check if all checkpoints are complete up to destination stage id, otherwise set to checkpoint completion stage it
|
game.stage_id=stage_element.stage_id;
|
||||||
game.stage_id=stage_element.stage_id;
|
}
|
||||||
}
|
match &stage_element.behaviour{
|
||||||
match &stage_element.behaviour{
|
model::StageElementBehaviour::SpawnAt=>None,
|
||||||
crate::model::StageElementBehaviour::SpawnAt=>None,
|
model::StageElementBehaviour::Trigger
|
||||||
crate::model::StageElementBehaviour::Trigger
|
|model::StageElementBehaviour::Teleport=>{
|
||||||
|crate::model::StageElementBehaviour::Teleport=>{
|
//I guess this is correct behaviour when trying to teleport to a non-existent spawn but it's still weird
|
||||||
//I guess this is correct behaviour when trying to teleport to a non-existent spawn but it's still weird
|
teleport_to_spawn(body,touching,style,modes.get_mode(stage_element.mode_id)?,models,game.stage_id)
|
||||||
|
},
|
||||||
|
model::StageElementBehaviour::Platform=>None,
|
||||||
|
&model::StageElementBehaviour::Checkpoint=>{
|
||||||
|
// let mode=modes.get_mode(stage_element.mode_id)?;
|
||||||
|
// if mode.ordered_checkpoint_id.map_or(true,|id|id<game.next_ordered_checkpoint_id)
|
||||||
|
// &&mode.unordered_checkpoint_count<=game.unordered_checkpoints.len() as u32{
|
||||||
|
// //pass
|
||||||
|
None
|
||||||
|
// }else{
|
||||||
|
// //fail
|
||||||
|
// teleport_to_spawn(body,touching,style,modes.get_mode(stage_element.mode_id)?,models,game.stage_id)
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
&model::StageElementBehaviour::Ordered{checkpoint_id}=>{
|
||||||
|
if checkpoint_id<game.next_ordered_checkpoint_id{
|
||||||
|
//if you hit a checkpoint you already hit, nothing happens
|
||||||
|
None
|
||||||
|
}else if game.next_ordered_checkpoint_id==checkpoint_id{
|
||||||
|
//if you hit the next number in a sequence of ordered checkpoints
|
||||||
|
//increment the current checkpoint id
|
||||||
|
game.next_ordered_checkpoint_id+=1;
|
||||||
|
None
|
||||||
|
}else{
|
||||||
|
//If you hit an ordered checkpoint after missing a previous one
|
||||||
teleport_to_spawn(body,touching,style,modes.get_mode(stage_element.mode_id)?,models,game.stage_id)
|
teleport_to_spawn(body,touching,style,modes.get_mode(stage_element.mode_id)?,models,game.stage_id)
|
||||||
},
|
}
|
||||||
crate::model::StageElementBehaviour::Platform=>None,
|
},
|
||||||
&crate::model::StageElementBehaviour::Checkpoint=>{
|
model::StageElementBehaviour::Unordered=>{
|
||||||
// let mode=modes.get_mode(stage_element.mode_id)?;
|
//count model id in accumulated unordered checkpoints
|
||||||
// if mode.ordered_checkpoint_id.map_or(true,|id|id<game.next_ordered_checkpoint_id)
|
game.unordered_checkpoints.insert(model_id);
|
||||||
// &&mode.unordered_checkpoint_count<=game.unordered_checkpoints.len() as u32{
|
None
|
||||||
// //pass
|
},
|
||||||
None
|
&model::StageElementBehaviour::JumpLimit(jump_limit)=>{
|
||||||
// }else{
|
//let count=game.jump_counts.get(&model.id);
|
||||||
// //fail
|
//TODO
|
||||||
// teleport_to_spawn(body,touching,style,modes.get_mode(stage_element.mode_id)?,models,game.stage_id)
|
None
|
||||||
// }
|
},
|
||||||
},
|
}
|
||||||
&crate::model::StageElementBehaviour::Ordered{checkpoint_id}=>{
|
}).or_else(||
|
||||||
if checkpoint_id<game.next_ordered_checkpoint_id{
|
match wormhole{
|
||||||
//if you hit a checkpoint you already hit, nothing happens
|
Some(gameplay_attributes::Wormhole{destination_model})=>{
|
||||||
None
|
|
||||||
}else if game.next_ordered_checkpoint_id==checkpoint_id{
|
|
||||||
//if you hit the next number in a sequence of ordered checkpoints
|
|
||||||
//increment the current checkpoint id
|
|
||||||
game.next_ordered_checkpoint_id+=1;
|
|
||||||
None
|
|
||||||
}else{
|
|
||||||
//If you hit an ordered checkpoint after missing a previous one
|
|
||||||
teleport_to_spawn(body,touching,style,modes.get_mode(stage_element.mode_id)?,models,game.stage_id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crate::model::StageElementBehaviour::Unordered=>{
|
|
||||||
//count model id in accumulated unordered checkpoints
|
|
||||||
game.unordered_checkpoints.insert(model_id);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
&crate::model::StageElementBehaviour::JumpLimit(jump_limit)=>{
|
|
||||||
//let count=game.jump_counts.get(&model.id);
|
|
||||||
//TODO
|
|
||||||
None
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(crate::model::TeleportBehaviour::Wormhole(wormhole))=>{
|
|
||||||
let origin_model=models.model(model_id);
|
let origin_model=models.model(model_id);
|
||||||
let destination_model=models.get_wormhole_model(wormhole.destination_model_id)?;
|
let destination_model=models.get_wormhole_model(destination_model)?;
|
||||||
//ignore the transform for now
|
//ignore the transform for now
|
||||||
Some(teleport(body,touching,models,style,body.position-origin_model.transform.translation+destination_model.transform.translation))
|
Some(teleport(body,touching,models,style,body.position-origin_model.transform.translation+destination_model.transform.translation))
|
||||||
}
|
}
|
||||||
None=>None,
|
None=>None,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
||||||
@ -1193,14 +1170,14 @@ impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
|||||||
let mut v=self.body.velocity;
|
let mut v=self.body.velocity;
|
||||||
let normal=contact_normal(&self.models,&style_mesh,contact);
|
let normal=contact_normal(&self.models,&style_mesh,contact);
|
||||||
match &contacting.contact_behaviour{
|
match &contacting.contact_behaviour{
|
||||||
Some(crate::model::ContactingBehaviour::Surf)=>println!("I'm surfing!"),
|
Some(gameplay_attributes::ContactingBehaviour::Surf)=>println!("I'm surfing!"),
|
||||||
Some(crate::model::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
|
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
|
||||||
&Some(crate::model::ContactingBehaviour::Elastic(elasticity))=>{
|
&Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{
|
||||||
//velocity and normal are facing opposite directions so this is inherently negative.
|
//velocity and normal are facing opposite directions so this is inherently negative.
|
||||||
let d=normal.dot(v)*(Planar64::ONE+Planar64::raw(elasticity as i64+1));
|
let d=normal.dot(v)*(Planar64::ONE+Planar64::raw(elasticity as i64+1));
|
||||||
v+=normal*(d/normal.dot(normal));
|
v+=normal*(d/normal.dot(normal));
|
||||||
},
|
},
|
||||||
Some(crate::model::ContactingBehaviour::Ladder(contacting_ladder))=>{
|
Some(gameplay_attributes::ContactingBehaviour::Ladder(contacting_ladder))=>{
|
||||||
if contacting_ladder.sticky{
|
if contacting_ladder.sticky{
|
||||||
//kill v
|
//kill v
|
||||||
//actually you could do this with a booster attribute :thinking:
|
//actually you could do this with a booster attribute :thinking:
|
||||||
@ -1227,16 +1204,16 @@ impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
|||||||
//check ground
|
//check ground
|
||||||
self.touching.insert(c);
|
self.touching.insert(c);
|
||||||
//I love making functions with 10 arguments to dodge the borrow checker
|
//I love making functions with 10 arguments to dodge the borrow checker
|
||||||
run_teleport_behaviour(&general.teleport_behaviour,&mut self.game,&self.models,&self.modes,&self.style,&mut self.touching,&mut self.body,model_id);
|
run_teleport_behaviour(&general.teleport_behaviour,&mut self.mode_state,&self.models,&self.modes,&self.style,&mut self.touching,&mut self.body,model_id);
|
||||||
//flatten v
|
//flatten v
|
||||||
self.touching.constrain_velocity(&self.models,&style_mesh,&mut v);
|
self.touching.constrain_velocity(&self.models,&style_mesh,&mut v);
|
||||||
match &general.booster{
|
match &general.booster{
|
||||||
Some(booster)=>{
|
Some(booster)=>{
|
||||||
//DELETE THIS when boosters get converted to height machines
|
//DELETE THIS when boosters get converted to height machines
|
||||||
match booster{
|
match booster{
|
||||||
&crate::model::GameMechanicBooster::Affine(transform)=>v=transform.transform_point3(v),
|
&gameplay_attributes::Booster::Affine(transform)=>v=transform.transform_point3(v),
|
||||||
&crate::model::GameMechanicBooster::Velocity(velocity)=>v+=velocity,
|
&gameplay_attributes::Booster::Velocity(velocity)=>v+=velocity,
|
||||||
&crate::model::GameMechanicBooster::Energy{direction: _,energy: _}=>todo!(),
|
&gameplay_attributes::Booster::Energy{direction: _,energy: _}=>todo!(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None=>(),
|
None=>(),
|
||||||
@ -1250,12 +1227,12 @@ impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
|||||||
match &general.trajectory{
|
match &general.trajectory{
|
||||||
Some(trajectory)=>{
|
Some(trajectory)=>{
|
||||||
match trajectory{
|
match trajectory{
|
||||||
crate::model::GameMechanicSetTrajectory::AirTime(_) => todo!(),
|
gameplay_attributes::SetTrajectory::AirTime(_) => todo!(),
|
||||||
crate::model::GameMechanicSetTrajectory::Height(_) => todo!(),
|
gameplay_attributes::SetTrajectory::Height(_) => todo!(),
|
||||||
crate::model::GameMechanicSetTrajectory::TargetPointTime { target_point: _, time: _ } => todo!(),
|
gameplay_attributes::SetTrajectory::TargetPointTime { target_point: _, time: _ } => todo!(),
|
||||||
crate::model::GameMechanicSetTrajectory::TargetPointSpeed { target_point: _, speed: _, trajectory_choice: _ } => todo!(),
|
gameplay_attributes::SetTrajectory::TargetPointSpeed { target_point: _, speed: _, trajectory_choice: _ } => todo!(),
|
||||||
&crate::model::GameMechanicSetTrajectory::Velocity(velocity)=>v=velocity,
|
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>v=velocity,
|
||||||
crate::model::GameMechanicSetTrajectory::DotVelocity { direction: _, dot: _ } => todo!(),
|
gameplay_attributes::SetTrajectory::DotVelocity { direction: _, dot: _ } => todo!(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None=>(),
|
None=>(),
|
||||||
@ -1271,7 +1248,7 @@ impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
|||||||
(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
|
//I think that setting the velocity to 0 was preventing surface contacts from entering an infinite loop
|
||||||
self.touching.insert(c);
|
self.touching.insert(c);
|
||||||
run_teleport_behaviour(&general.teleport_behaviour,&mut self.game,&self.models,&self.modes,&self.style,&mut self.touching,&mut self.body,model_id);
|
run_teleport_behaviour(&general.teleport_behaviour,&mut self.mode_state,&self.models,&self.modes,&self.style,&mut self.touching,&mut self.body,model_id);
|
||||||
},
|
},
|
||||||
_=>panic!("invalid pair"),
|
_=>panic!("invalid pair"),
|
||||||
}
|
}
|
||||||
@ -1378,17 +1355,17 @@ impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
|
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
|
||||||
let h0=Hitbox::from_mesh_scale(PhysicsMesh::from(&crate::primitives::unit_cube()),Planar64Vec3::int(5,1,5)/2);
|
let h0=gameplay_style::Hitbox::from_mesh_scale(PhysicsMesh::from(&crate::primitives::unit_cube()),Planar64Vec3::int(5,1,5)/2);
|
||||||
let h1=Hitbox::roblox();
|
let h1=gameplay_style::Hitbox::roblox();
|
||||||
let hitbox_mesh=h1.transformed_mesh();
|
let hitbox_mesh=h1.transformed_mesh();
|
||||||
let platform_mesh=h0.transformed_mesh();
|
let platform_mesh=h0.transformed_mesh();
|
||||||
let minkowski=crate::model_physics::MinkowskiMesh::minkowski_sum(&platform_mesh,&hitbox_mesh);
|
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(&platform_mesh,&hitbox_mesh);
|
||||||
let collision=minkowski.predict_collision_in(&relative_body,Time::MAX);
|
let collision=minkowski.predict_collision_in(&relative_body,Time::MAX);
|
||||||
assert_eq!(collision.map(|tup|tup.1),expected_collision_time,"Incorrect time of collision");
|
assert_eq!(collision.map(|tup|tup.1),expected_collision_time,"Incorrect time of collision");
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
|
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
|
||||||
let h0=Hitbox::new(PhysicsMesh::from(&crate::primitives::unit_cube()),
|
let h0=gameplay_style::Hitbox::new(PhysicsMesh::from(&crate::primitives::unit_cube()),
|
||||||
integer::Planar64Affine3::new(
|
integer::Planar64Affine3::new(
|
||||||
integer::Planar64Mat3::from_cols(
|
integer::Planar64Mat3::from_cols(
|
||||||
Planar64Vec3::int(5,0,1)/2,
|
Planar64Vec3::int(5,0,1)/2,
|
||||||
@ -1398,10 +1375,10 @@ fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time
|
|||||||
Planar64Vec3::ZERO,
|
Planar64Vec3::ZERO,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let h1=Hitbox::roblox();
|
let h1=gameplay_style::Hitbox::roblox();
|
||||||
let hitbox_mesh=h1.transformed_mesh();
|
let hitbox_mesh=h1.transformed_mesh();
|
||||||
let platform_mesh=h0.transformed_mesh();
|
let platform_mesh=h0.transformed_mesh();
|
||||||
let minkowski=crate::model_physics::MinkowskiMesh::minkowski_sum(&platform_mesh,&hitbox_mesh);
|
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(&platform_mesh,&hitbox_mesh);
|
||||||
let collision=minkowski.predict_collision_in(&relative_body,Time::MAX);
|
let collision=minkowski.predict_collision_in(&relative_body,Time::MAX);
|
||||||
assert_eq!(collision.map(|tup|tup.1),expected_collision_time,"Incorrect time of collision");
|
assert_eq!(collision.map(|tup|tup.1),expected_collision_time,"Incorrect time of collision");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user